Zig: get rid of the `.` in tuples & anonymous struct literal syntax

Created on 14 Apr 2020  Â·  16Comments  Â·  Source: ziglang/zig

I'm not sure if it's possible to do this, but here's an issue at least to document that we tried.

I believe the current problem is that when the parser sees

{ expression

It still doesn't know if this is is a block or tuple. But the next token would either be , making it a tuple, ; making it a block, or } making it a tuple. Maybe that's OK.

Then tuples & anonymous struct literals will be less noisy syntactically and be easier to type.

breaking proposal

Most helpful comment

Oh, I think there's also an ambiguous case. These two statements are both valid but mean very different things:

fn voidErrorFn() error{MaybeError}!void {}
comptime {
    var x = voidErrorFn() catch .{};
    var y = voidErrorFn() catch {};
}

All 16 comments

This would be nicer to type, but the ambiguity with blocks could lead to some unintuitive compile errors. Consider this example:

fn voidFn() void {}

export fn usefulError(foo: bool) void {
    if (foo) {
        voidFn() // forgot semicolon here
    } else {
//  ^ current compile error: expected token ';', found '}' on first token after forgotten semicolon
        voidFn();
    }
}

Here I've forgotten the semicolon on the expression in the if statement, and the compiler gives an error at the first token after the missing semicolon. But with this proposal, the missing semicolon would cause the block to be interpreted as a tuple, equivalent to this code:

fn voidFn() void {}

export fn weirdError(foo: bool) void {
    if (foo) .{
//  ^ error: incompatible types: 'struct:42:15' and 'void'
//  ^ note: type 'struct:42:15' here
        voidFn() // forgot semicolon here
    } else {
//         ^ note: type 'void' here
        voidFn();
    }
}

For a forgotten semicolon, this is a very confusing error to get, especially for someone new to the language who hasn't learned anonymous struct syntax yet. This might be a solvable problem by identifying cases where this parsing is close to ambiguous and improving the error messages that result, but the two interpretations are different enough that it might be difficult to give an error message that is meaningful for both.

Oh, I think there's also an ambiguous case. These two statements are both valid but mean very different things:

fn voidErrorFn() error{MaybeError}!void {}
comptime {
    var x = voidErrorFn() catch .{};
    var y = voidErrorFn() catch {};
}

Good point on the ambiguous case, and good explanation in the other comment.

For the ambiguous case I'd be willing to change {} to mean "tuple of 0 values / struct with 0 fields" and find some other way to specify the void value. @as(void, undefined) already works, but is a bit scary. Maybe void{} if #5038 is rejected.

Mixing blocks and tuples&structs is the very reason I don't like this proposal. I often forgets the brackets .{ } for printf functions but I never forgot the little dot. What new features are blocked by this proposal?

What new features are blocked by this proposal?

nothing - this is pure syntax bikeshedding :bicyclist:

I'd be willing to change {} to mean "tuple of 0 values / struct with 0 fields" and find some other way to specify the void value.

Hmm, that would definitely work, but it also might have the side effect of unexpectedly converting

fn voidErrorFn() error{MaybeError}!void {}
export fn foo() void {
    voidErrorFn() catch {
        //commentedOutCall();
    };
}

to

fn voidErrorFn() error{MaybeError}!void {}
export fn foo() void {
    voidErrorFn() catch .{};
}

when the programmer tries to test commenting out that line.

This could potentially be solved, but we would have to go down kind of a strange road. Zig currently makes a distinction for many types of blocks between expressions and statements. The if in x = if (foo) { } else { }; is an expression. As a result, it requires a semicolon. But if (foo) { } else { } is not an expression but a full statement, so it does not require a semicolon. If we changed <expression_evaluating to !void> catch { code block } to a full statement, so it wouldn't require a semicolon, we could then define {} to mean empty block in a statement context and empty struct in an expression context. This would be kind of a weird facet of the language to play into, but it would fix all of the problems I've brought up and replace them with a single weird but consistent thing. In most of the cases I've thought through, this definition can produce a useful error message if you accidentally get the wrong interpretation of {}. But this could still end up being a footgun in comptime code.

Maybe we can change block syntax, #4412

OK, I'm convinced to close this in favor of status quo.

Another option - maybe already discussed, I couldn't find any discussion about it yet - is to use [] for the anonymous list literals (and tuples).

Inspired by ({}) in JavaScript and (0,) in Rust:

Idea No.1:

{}              // empty block -> void
({})            // still empty block -> void
                   because block is also expression instead of statement
{,}             // empty tuple/struct
{a} {a,b} {...} // tuple with N elements
{.a = x, ...}   // struct
{...;}          // block -> void
label: {...}    // block

const a = [_]u8{1, 2, 3};
const a: [3]u8 = {1, 2, 3};
const a = @as([3]u8, {1, 2, 3});
const a = [_]u8{};  // error?
const a = [_]u8{,};
const a = @as([0]u8, {});  // error!
const a = @as([0]u8, {,});

fn multiReturn() anyerror!(A, B) { ... return {a, b}; ... }
Idea No.2:

()              // empty tuple
(,)             // empty tuple
(,a)            // tuple with 1 element
(a,)            // tuple with 1 element
(a, b, ...)     // tuple with N elements

{}              // empty block (asymmetric to tuple syntax)
{,}             // empty struct
{, ...}         // struct
{...}           // block/struct
label: {...}    // block
({})            // still empty block -> void

const a: [_]u8 = (1, 2, 3);
const a = @as([3]u8, (1, 2, 3));

const a: T = {};  // error if T is not void?
const a: T = {,};
const a = @as(T, {});  // error if T is not void?
const a = @as(T, {,});

const a = [_]u8.(1, 2, 3);
const a = T.{};
↑ Rejected :P https://github.com/ziglang/zig/issues/760#issuecomment-430009129

const a = [_]u8{1, 2, 3};
const a = T{};
↑ Don't worry? https://github.com/ziglang/zig/issues/5038

fn multiReturn() anyerror!(A, B) { ... return (a, b); ... }

A really cool but probably bad option is to define the single value of void as the empty struct. This would remove the ambiguity since it doesn't matter if {} is a block or an empty struct, they both evaluate to the value of void! But this could cause a lot of other weirdness, like var x: Struct = voidFn(); causing x to be default-initialized, so it's probably not something we should do.

I suggest to reconsider about this proposal.

As @andrewrk noticed in 0.6.0 release note, 0.7.x maybe the last chance to make a bigger change in zig to polish this language more elegant, intuitive, simpler. There are a lot of proposals related to this:

4294 always require brackets, so it reduces use cases about block and resolves issue #1347. Also related to #5042, #1659 which can remove () used in if/while/for/switch.

4412 new keyword seqblk for labeled blocks, may be block should use this kind of syntax.

4170 shows in order to keep consistency, anonymous funtion literal has a weird syntax.

5038 removes T{}, also related to #4847, array default initialization ayntax.

4661 remove bare catch, related to require brackets and examples below.

IMO we should take all these things into account. Here's the ideally syntax what I prefer to:

// named block
const x = block blk {
    var a = 0;
    break :blk a;
};

block outer while {
    while {
        continue :outer;
    }
}

// unnamed block
block {
    var a = 0;
    assert(a+1 == 1);
}

// switch
const b = switch {
    a > 0 => 1,
    else => 0,
}

const b = switch var a = calc() {
    a == 0 || a == 1 => 0,
    else => 1,
};

switch var a = calc(); a {
    .success => block {
        warn("{}", {a});
    },
    else => @panic(""),
}

// while
var i: usize = 0;
while {
    i += 1;
    if i < 10 { continue; }
    break;
}
assert(i == 10);

var i: usize = 1;
var j: usize = 1;
while i * j < 2000; i *= 2, j *= 3 {
    const my_ij = i * j;
    assert(my_ij < 2000);
}

const x = while var i = 0; i < end ; i += 1 {
    if i == number { break true; }
} else { false }

while var i = 0; getOptions(i) => v; i += 1 {
    sum += v;
} else {
    warn("", {});
}

// for
for seq => v {
    warn("{}", {v});
}

// if
if var a = 0; a != b {
    assert(true);
} else if a == 9 {
    unreachable;
} else {
    unreachable;
}

const a = if b { c } else { d };

const x: [_]u8 = if a => value {
    {value, 1, 2, 3, 4}
} else block blk {
    warn("default init", {});
    break :blk {0, 1, 2, 3, 4};
}

// error
const number = parseU64(str, 10) catch { unreachable };

const number = parseU64(str, 10) catch { 13 };

const number = parseU64(str, 10) catch block blk {
    warn("", {});
    break :blk 13;
};

const number = parseU64(str, 10) catch err switch err {
    else => 13,
};

fn foo(str: []u8) !void {
    const number = parseU64(str, 10) catch err { return err };
}

if parseU64(str, 10) => number {
    doSomethingWithNumber(number);
} else err switch err {
    error.Overflow => block {
        // handle overflow...
    },
    error.InvalidChar => unreachable,
}

errdefer warn("got err", {});

errdefer err if errFormater => f {
    warn("got err: {}", {f(err)});
}

// tuple & anonymous literals
var array: [_:0]u8 = {11, 22, 33, 44};

const mat4x4: [_][_]f32 = {
    { 1.0, 0.0, 0.0, 0.0 },
    { 0.0, 1.0, 0.0, 1.0 },
    { 0.0, 0.0, 1.0, 0.0 },
    { 0.0, 0.0, 0.0, 1.0 },
};

var obj: Object = {
    .x = 13,
    .y = 67,
    .item = { 1001, 1002, 1003 },
    .baseProp = { .hp = 100, .mp = 0 },
};

@SpexGuy Similar option: syntactically {} --> block/struct/tuple but void value -/-> block/struct/tuple? something else to consider...

fn baz() void {}  // ok

// expect return_type but found anonymous struct/list literal {}
fn baz() {} {}

// foo({}) --> bar is always a struct and never a void value?
// foo(@as(void, undefined)) for the rescue
fn foo(bar: var) var { ... }

// any other place to disallow empty blocks?
printf("{}", {})  // error: Too few arguments

@mogud I haven't read all those issues but the design looks quite messy...

while{}  // instead of while true{} or loop{} (yet another keyword!)

// how about "switch true {...}"?
// it duplicates the function of if/else
const b = switch {
    a > 0 => 1,
    else => 0,
}

// mind-blown by the use of ";"
switch var a = calc(); a {...}
if var a = 0; a != b {...}
while i * j < 2000; i *= 2, j *= 3 {...}
while var i = 0; i < end ; i += 1 {...}

// different use of "=>" from "switch"
if parseU64(str, 10) => number {...}
for seq => v {...}
while var i = 0; getOptions(i) => v; i += 1 {
    sum += v
} else {
    // why no semicolon?
    warn("", {})
}

const x: [_]u8 = if a => value {
    // disallow multiple statements?
    {value, 1, 2, 3, 4}
} else block blk {
    warn("default init", {});
    // can it be just {0,1,2,3,4} (without semicolon)?
    break :blk {0, 1, 2, 3, 4};
}

const x = while var i = 0; i < end ; i += 1 {
    if i == number { break true; }
} else false;  // why no brackets?

@iology sorry for those type mistakes, I've edited the post.

const x: [_]u8 = if a => value {
    // disallow multiple statements?
    //      -> no, this is and only can be a single expression
    {value, 1, 2, 3, 4}
} else block blk {
    warn("default init", {});
    // can it be just {0,1,2,3,4} (without semicolon)?
    //      -> no, only catch/if/else used as expression can have a single expression within `{}`.
    break :blk {0, 1, 2, 3, 4};
}

@mogud I truly do appreciate what you're doing here, but I don't think the syntax is going to go that direction.

That's ok, zig itself is more important for me. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andrewrk picture andrewrk  Â·  3Comments

andersfr picture andersfr  Â·  3Comments

andrewrk picture andrewrk  Â·  3Comments

dobkeratops picture dobkeratops  Â·  3Comments

S0urc3C0de picture S0urc3C0de  Â·  3Comments