ziglang / zig

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

Proposal: add another form of `@compileError` to support more sophisticated error reporting #21685

Closed gabeuehlein closed 2 weeks ago

gabeuehlein commented 2 weeks ago

Introduction

Currently, user-written Zig code (including the standard library) can only fail compilation with a descriptive message through @compileError. There are several cases in which it would be useful to be able to report more than just a main message, such as minor contextual information that caused the error or the (approximate) source location of an offending variable declaration. This proposal aims to address this by suggesting an implementation of an extended form of @compileError that supports emitting ancillary information in a cleaner way, making it easier to both understand compile errors from a user's perspective and create them from a library author's perspective.

Rationale

An extended form of @compileError would allow library authors to give extra information regarding why a compile error was triggered to users in order help guide them toward resolving the error. An example of an error message that could be improved by this proposal lies in lib/std/fmt.zig: https://github.com/ziglang/zig/blob/7e530c13b3ef9b61417a610c00fc1d37c11ff7ed/lib/std/fmt.zig#L531-L535 Here, an error is emitted that says that an error union cannot be formatted without an appropriate specifier. While that's correct, it is rather unhelpful in that it doesn't tell the user where the error union is, even though it is known at comptime. For instance, in the code below, it is quite obvious that the !u32 return type for int is causing the problem, and that it has the index 1 in the format arguments.

const std = @import("std");

pub fn string() []const u8 {
    return "hello there";
}

pub fn int() !u32 {
    return 18;
}

pub fn foo() void {
    std.debug.print("The string is \"{s}\" and the int is {d}\n", .{ string(), int() });
}

comptime {
    // needed to force analysis of `foo`
    _ = &foo;
}

To a newer Zig user, the error message currently emitted may be too ambiguous and slow their ability to learn the language. A way this could be "fixed" in the status quo is by adding the argument index directly to the error message:

lib/std/fmt.zig:528:17: error: cannot format error union at index 1 without a specifier (i.e. {!} or {any})
                @compileError("cannot format error union at index " ++ comptimePrint("{d}", .{index}) ++ " without a specifier (i.e. {!} or {any})");
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This, however, produces three excessively long lines, which can be hard to read. Furthermore, these lines would only become longer if even more information is added to try to guide users, such as suggesting the usage of try or catch unreachable. It would be better if the extra information is placed on separate lines as error notes, which would be both easier for a user to read and for a library author to write (as my proposal below wouldn't rely on comptime string manipulation like what is needed currently).

Proposal

Add a new builtin, @compileErrorExt, that takes a single parameter. This parameter is of a new type, CompileErrorExtData, defined in std.builtin:

const CompileErrorExtData = struct {
    msg: [:0]const u8,
    notes: []const [:0]const u8 = &.{},
    // Other fields may be added here in the future. For example, `decl_locations`
    // may be added to enable the reporting of the source locations for any declarations
    // that contributed to the error being emitted.
};

@compileErrorExt would then be called as so:

    @compileErrorExt(.{
        .msg = "cannot format error union without a specifier (i.e. {!} or {any})",
        .notes = &.{
            "the bad argument is located at index " ++ comptimePrint("{d}", .{index}),
            "if you didn't want to format an error union, handle the error using try or catch",
        },
    });

Reaching the code above would then produce output similar to the following:

$ zig build-obj proposal.zig
lib/std/fmt.zig:123:17: error: cannot format error union without a specifier (i.e. {!} or {any})
                @compileErrorExt(.{
                ^~~~~~~~~~~~~~~~~~~
lib/std/fmt.zig:123:17: note: the bad argument is located at index 1
lib/std/fmt.zig:123:17: note: if you didn't want to format an error union, handle the error using try or catch

Additional Questions/Alternative Implementations

  1. Should @compileErrorExt take a list of parameters instead?
    • This might be simpler to implement (see the PoC I made a few weeks ago), but may not be backwards-compatible if @compileErrorExt isn't made variadic.
    • I personally don't like the idea of passing a bunch of magic parameters to a builtin, especially one that may be expanded later; we already have @Type which takes an std.builtin.Type, which is easy to read due to having named fields.
  2. What other functionality could @compileErrorExt have?
    • Besides the additional functionality I discussed here (error notes and source location reporting), I could also see a feature like pointing to and describing specific parts of a large span of code (a lá Rust's error messages or warnings) being a reasonable function of @compileErrorExt. AFICT, the Zig compiler doesn't have the infrastructure to support this (yet), so this would have to be a future addition.
  3. Should CompileErrorExtData be a union instead?
    • There are different "styles" of reporting an error at compile time based on the context of the code that caused it (see question 2 for an example of what I'm calling a "style"). Therefore, it might be a better approach to make CompileErrorExtData a union(enum) instead to group the information required for each error reporting style more nicely.
      • This would require naming each "style," so names for those would have to be discussed if this approach is taken.
    • Again, @Type takes a tagged union parameter, so this shouldn't be too odd of an addition to the language and shouldn't require that much extra work to be done internally.

Other suggestions or additions are welcome.

nektro commented 2 weeks ago

Duplicate of https://github.com/ziglang/zig/issues/12093

mlugg commented 2 weeks ago

Closing as duplicate