ziglang / zig

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

Proposal: `++=` and `**=` operators #5804

Closed ghost closed 4 years ago

ghost commented 4 years ago

For concatenate-assign and repeat-assign, respectively. Honestly surprised we don't already have these. Would make it simpler to, for instance, assemble a formatted string at comptime for printing in one go rather than making separate calls for each component.

pixelherodev commented 4 years ago

Mind explaining what this means in a bit more detail?

Sobeston commented 4 years ago

Mind explaining what this means in a bit more detail?

Just a shorthand for x = x ++ y and x = x ** y, with x ++= y and x **= y.

Another potential one: ||= for when you're expanding an error set, albeit not as common.

ghost commented 4 years ago

Upon further consideration, we don't have a viable mechanism for how this would need to interact with the type system. Closing until we do.

Vexu commented 4 years ago

Upon further consideration, we don't have a viable mechanism for how this would need to interact with the type system.

What do you mean? How would this be any different than using ++/**?

ghost commented 4 years ago

The result of ++ or ** is a different type from either of its operands -- its length is different. Thus, a variable that holds both an operand and the result would have to be length-polymorphic, which is not possible, and rightly so.

Vexu commented 4 years ago

It is possible with slices.

SpexGuy commented 4 years ago

It is also possible with anytype fields.

ghost commented 4 years ago

This use case can be accomplished much more cleanly and without AMM with #5873 and an ArrayList.

SpexGuy commented 4 years ago

Doing this with slices incurs a hidden memory allocation. I don't care if it's at comptime, those are bad.

Lots of things do hidden dynamic allocation at comptime. Like this:

const x = 4;
const y: [x]u32 = ...;

Here the length of y is not known until the compiler is running, hence this is a dynamic allocation. Even the declaration of x is a dynamic allocation, because comptime_int is a variable length type. "hidden allocation is bad" is often true, but not always. What are the actual flaws with this approach at comptime? The usual reasons to avoid dynamic allocations are:

None of these apply at comptime. Comptime code cannot fail in production because it doesn't run in production, it only runs when compiling. All allocations are inherently dynamic in the compiler because it doesn't know the size of anything until it starts reading the code, so there is no meaningful difference in performance cost between a comptime stack allocation, a comptime global declaration, and a comptime dynamic allocation. Comptime allocations do not need to be cleaned up because they are garbage collected. This also makes use-after-free impossible at comptime, which is massive.


In my mental model, anytype represents a value with an unknown type at define time, but which is instantiated with a single static type at call time. As an explicit inline change of type, this does not fit into that.

This is probably because your mental model is trained on function parameters, which are const. anytype fields in structs can change type over their lifetime. This feature is necessary. It is used by std.builtin.TypeInfo.Pointer to represent the sentinel. In order to robustly construct the type *<modifiers>[1]T from *<modifiers>T, for example, modifiers on the pointer need to be preserved. The maintainable approach is to use @typeInfo to get a type info for the pointer and modifiers, alter the target type of the pointer, and then use @Type to construct the altered type. But in order to do this, the type of the sentinel field must change from ?T to ?[1]T. This type mutability is also the only way to build a tuple when the number of items is comptime-known but not lexically known. For example, iterating over function parameter type info to generate a tuple to pass to @call. Without this ability, you would need to switch on the number of arguments and write a different tuple declaration in each case. A comptime ArrayList can never accomplish this because the items in a tuple have different lengths, so a comptime allocator would never be able to dynamically append to a tuple while preserving type safety.


I think the current comptime differs significantly from what you want it to be. Making lots of small tickets like 5873 and 5718 will not inspire that change, because each does not make sense on its own. There may be some merit to your alternate design of comptime, with no global lifetimes, but it will require many language changes to happen simultaneously. At an abstract level, unifying comptime and runtime semantics seems like it would be a major boon. On the other hand, the more flexible scripting-language-like semantics of comptime are very nice and useful. I think what's needed is a thorough and complete description of your ideal comptime semantics, including comptime scoping rules, behavior of constants, interactions with comptime-only types, and a strong case that your design is at least as powerful as the current definition and makes the language simpler. This would allow people to compare the two ideas, suggest feedback, and properly discuss the tradeoffs involved. Without that, I think the individual smaller changes you are recommending will be shot down because they don't fit with the rest of comptime semantics.

ghost commented 4 years ago

Will do. Mind reminding me tomorrow? I need to distance myself for now.

ghost commented 4 years ago

5895 is up. Feedback welcome.