Skip to content

Make assignment operations expressions #11805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ifreund opened this issue Jun 6, 2022 · 12 comments
Open

Make assignment operations expressions #11805

ifreund opened this issue Jun 6, 2022 · 12 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@ifreund
Copy link
Member

ifreund commented Jun 6, 2022

Currently the following code snippets which look valid do not parse:

test {
    var x: u32 = 0;
    while (true) if (true) x += 1;

    var y = false;
    switch (y) {
        true => if (true) y = false,
        false => if (true) y = true,
    }
   
    defer if (true) y = false;
}

Some of these examples are mentioned in #5731 and #3749.

This is because the grammar does not treat assignment operations such as x = 1 or y += 5 as expressions and only allows them in specific contexts.

I propose changing the grammar to make assignment operations normal expressions, example patch here. I think this may unlock some further cleanups to the grammar as well not included in that example patch.

Assignment expressions would be defined to always evaluate to type/value void. In effect, we already allow expressions with exactly these semantics such as { x = 1; }. This proposal merely makes the grammar a bit more consistent and allows writing more intuitive code as the examples above demonstrate.

Original additional proposal to disallow void expressions in most contexts That change alone would however bring some downsides. For example code like this would become valid: ```zig test { var y: u32 = 0; var x = (y = 1); // declares variable x with type/value void, assigns 1 to y } ``` This is obviously undesirable, therefore I propose disallowing expressions of type void in all contexts except: 1. As a statement in a block. 2. As the operand of `defer`/`errdefer`/`suspend`. 4. In the continue expression of a while loop. 5. In the body and else branch of `if`/`while`/`for` if the expression as a whole evaluates to type void. 6. In `switch` cases if the switch as a whole evaluates to type void. 7. As the operand of `comptime`/`nosuspend` if the expression as a whole evaluates to type void.

Note that rules 4, 5, and 6 are transitive, the outer if/while/for/switch/comptime/nosuspend must also appear in a context where void expressions are permitted.

I also propose that an exception be made for the empty block {} which is permitted as an expression in all contexts as the canonical void value.

These proposed rules would make the var x = (y = 1); snippet a compile error while allowing the motivating examples above.

These rules would also effectively implement #9059. As discussed there, there are already cases in the language where allowing void expressions causes confusion and this proposal would help eliminate those.

@ifreund ifreund added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jun 6, 2022
@rohlem
Copy link
Contributor

rohlem commented Jun 6, 2022

Am I interpreting correctly that this proposal would similarly disallow function calls resulting in void in these contexts? For instance,

fn wrapper(target_fn: anytype) @typeInfo(target_fn).Fn.return_type.? {
  return target_fn();
}

would now only be valid for a target_fn with non-void return type?
(Though still possible to implement status-quo behaviour by special casing void to work around this.)

@ifreund
Copy link
Member Author

ifreund commented Jun 6, 2022

Am I interpreting correctly that this proposal would similarly disallow function calls resulting in void in these contexts?

Yes that's correct, this would disallow all expressions that evaluate to a void type including calls to functions with return type void. You're right that this will require some more special casing of void in generic code than the status quo requires. I think this is an acceptable tradeoff in your example but this brings more problematic cases to mind such as std.AutoHashMap(u32, void).

In implementing such a generic data structure, there might be a function like this:

fn put(self: *Self, key: K, value: V) void {
    self.items[i].key = key;
    self.items[i].value = value; // if V is void then the expression `value` has type void.
}

Adding another exception alongside {} for plain identifier expressions such as value in that example would solve that use-case. I'm trying to think of other particularly problematic cases.

@ifreund ifreund added this to the 0.11.0 milestone Jun 6, 2022
@ifreund ifreund added the breaking Implementing this issue could cause existing code to no longer compile or have different behavior. label Jun 6, 2022
@Hejsil
Copy link
Contributor

Hejsil commented Jun 6, 2022

I wounder if it is really worth the effort preventing people from using assignments in expressions. Currently, we allow function that return void everywhere, and you don't really see people using that for anything spicy, because it does not have much use.

@rohlem
Copy link
Contributor

rohlem commented Jun 6, 2022

Just an observation, all motivating examples listed here make use of if expressions (grammar element IfExpr).
From the problem description that assignment statements aren't expressions, to me it sounds like we could instead allow if statements (with block or statement bodies) in those places.
If that results in ambiguities, ban if expressions from the top level of expressions where if statements are also accepted.

I assume you found some downside with this, compared to the proposed solution?
EDIT: I guess that it doesn't solve #9059 in one fell swoop? And maybe the grammar grows by doing this?
Fwict errors earlier in the compiler pipeline are preferred, and checking types happens later, though I don't know how much complexity increase is reasonable for offsetting this.

@ifreund
Copy link
Member Author

ifreund commented Jun 6, 2022

I wounder if it is really worth the effort preventing people from using assignments in expressions. Currently, we allow function that return void everywhere, and you don't really see people using that for anything spicy, because it does not have much use.

This is a good point. We already allow { x = 42; } as an expression, which has the same semantics I'm proposing for the expression x = 42.

I may have gotten too hung up on trying to keep the language as strict or stricter than status quo at the cost of significantly increasing complexity. I think that the examples in #9059 show that there are some weird edge cases with the status quo of void expressions, but that is perhaps orthogonal to allowing usage of assignments as expressions.

I think I'll move the "disallow void expressions in most contexts" part of this proposal over to #9059 and reduce the scope of this to allowing assignment operations in expressions.

@ifreund ifreund changed the title Make assignment operations expressions, disallow void expressions in most contexts Make assignment operations expressions Jun 6, 2022
@ifreund ifreund removed the breaking Implementing this issue could cause existing code to no longer compile or have different behavior. label Jun 6, 2022
@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jul 9, 2023
@abvee
Copy link

abvee commented Sep 18, 2024

Hey,

Has there been any work done on this proposal ? As of zig 0.13.0, you still cannot do this:

var x: usize = 4;
for (0..10) |i|
	if (true) x = i;

You have to make the assignment an expression:

var x: usize = 4;
for (0..10) |i|
	if (true) {x = i;};

Are there any plans to make assignments the same as normal expressions ?

@InKryption
Copy link
Contributor

I think it's interesting to note that the main benefit of making assignments act as statements rather than as expressions is avoiding the pitfall seen in C where x == y can by typo'd as x = y, causing the condition to simply evaluate to the value of y, and by extension its truthiness or nonzero-ness.

However in zig this is already avoided/avoidable in two ways:

  • It doesn't have any concept of truthiness, only booleans are allowed as condition expressions.

  • Assignments (under this proposal, and simply as a sane choice in general) would evaluate to void, meaning the typo isn't a runtime bug, but a compile error which at worst will make the programmer feel a little silly.

In effect, status quo assignments-as-statements acts as a redundant protection against a solved problem, and incur an extra unnecessary cost in complexity in the grammar, and adds special cases to remember in general usage, ie while (cond) : (expr_or_assignment) {} vs (while (cond) : (expr) {}, plus the other special cases mentioned in this and related issues.

@rohlem
Copy link
Contributor

rohlem commented Sep 20, 2024

  • Assignments (under this proposal, and simply as a sane choice in general) would evaluate to void, meaning the typo isn't a runtime bug, but a compile error which at worst will make the programmer feel a little silly.

@InKryption While I generally agree with your point, the single exception I'll nitpick is generic contexts that allow both bool and void expressions.
For example an anytype function argument would allow both a == b (bool result) and a = b (immediate assignment, void result).
I do think it's good the second isn't allowed. In contrast, both a function call f() and a block {a = b;} may yield void, but look intentional and make sense to allow.

@abvee
Copy link

abvee commented Sep 23, 2024

For example an anytype function argument would allow both a == b (bool result) and a = b (immediate assignment, void result).

I'm still a student, so I might be ignorant on the topic. However, I cannot think of a function that is evaluated at compile time doing something that will not result in an error if passed a void type variable.

At least, from my understanding, anytype is generally followed by @hasDecl or @typeInfo, which should both give compile errors if it has to evaluate a void expression.

@rohlem
Copy link
Contributor

rohlem commented Sep 23, 2024

@abvee I mostly expect usage of void in generic code.
For example, a union(error) may have some fields with additional payload data, and some fields without - it's up to the implementation whether that is represented via an 0-bit empty struct{} or as void.
Status-quo allows a union field of void payload to coerce from enum literal syntax: .foo can be written instead of .{.foo = {}}, but not instead of .{.foo = .{}}.

You could also write a function to handle a function result, f.e. repackage an error union E!T into a different userland structure.
To me it seems sensible for such a function to accept both E!T as well as plain T, which could include both void and bool.

@Aldlevine
Copy link

I'm relatively new to the language, but would this not cause some potential confusion when creating structs?

This would be allowed:

var x = 0;
var y = 0;
var z = 0;
const my_struct = .{x = 1, y = 2, z = 3};

and my_struct would end up as the tuple .{1, 2, 3} while assigning the variables x, y, z to 1, 2, 3 respectively.

@rohlem
Copy link
Contributor

rohlem commented Nov 18, 2024

@Aldlevine The original post states "Assignment expressions would be defined to always evaluate to type/value void."
.{x = 1, y = 2, z = 3} would be equivalent to the expression .{{x = 1;}, {y = 2;}, {z = 3;}} in status-quo, and result in a tuple of three void values .{{}, {}, {}}.

@andrewrk andrewrk modified the milestones: 0.14.0, 0.15.0 Feb 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

7 participants