-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
@hide: a way to manually/prematurely end the scope of identifiers. #9792
Comments
This is also my first proposal here after having being an observer for more than a few months, so any pointers or critiques on anything I've done wrong here, or missed out on, would be much appreciated. |
This isn't quite true, for a few reasons.
However, these are not really problems IMO. This feature would be resolved entirely within astgen, which is the most parallel part of the compilation process, and also highly cacheable. It's also really not a large cost, hides would be a simple piece of data. So in this case I think performance considerations can be mostly ignored and we should instead focus on the ways in which this proposal would affect the language and best practices. |
@SpexGuy thank you, that's very insightful, and good to hear. I shall edit the proposal to reflect this. |
One general use case that comes to mind is when you have a variable that is typed "generically" but you know it must be a more specific type. A perfect example of this is how zig handles inheritance, where you get a pointer to the "generic base type" but that pointer is only used to get a pointer to a more specific type, i.e. const CustomAllocator = struct {
allocator: Allocator,
pub fn alloc(self: *Allocator) void {
const self = @fieldParentPtr(CustomAllocator, "allocator", @hide(self));
}
} Another example, say you are looping through a json Array where you know all the items are strings. In this case each element of the json array is a "generic" json Node, but once you have the more "specific" string instance, you no longer need a symbol pointing to the generic node: for (json_array.items) |*json_string| {
const json_string = @hide(json_string).String;
} Note that good error handling would still be possible: for (json_array.items) |*json_string| {
const json_string = verifyIsStringOrPrintGoodErrorAndExit(@hide(json_string));
} Or another example is passing a generic callback that accepts a pub fn myCallback(context: *void) {
const context = @ptrCast(*MyContext, @hide(context));
} I'm not sure this use case is as good as the "mutable" use case but it shows some ways a feature like this could be used. |
This would be useful for one of my projects where consistent unique ids are needed frequently, which change inside of loops fn someFunction(id: ID) {
const id = @hide(id).pushFunction(@src());
for(array) |item| {
const id = @hide(id).pushPointer(@src(), item);
}
} Mutability can be used for this, but using constants feels much nicer and less error-prone fn someFunction(id: *IDStack) {
id.pushFunction(@src());
defer id.pop();
for(array) |item| {
id.pushPointer(@src(), item);
defer id.pop();
}
} Currently, using constants requires messier, more error-prone code like this fn someFunction(id: ID) {
const id_outer = id.pushFunction(@src);
for(array) |item| {
const id_inner = id_outer.pushPointer(@src(), item);
}
} |
@pfgithub I'm a little bit confused by your example, I can't quite imagine what the context for something like this would be. What does |
@InKryption Here is a more realistic example, actually making use of ids: Improved example (hopefully)This is for generating unique ids for an imgui library, allowing storing data and computations across frames. fn renderForm(id_arg: ID.Arg) Widget {
const id_outer = id_arg.id;
var v_layout = VerticalLayout{ .gap = 2 };
v_layout.put(renderText(id_outer.push(@src()), "Contact Information:"));
for (.{
"Name", "Email Address", "Phone Number",
}) |label, i| {
const id_inner = id.pushIndex(@src(), i);
v_layout.put(renderInput(id_inner.push(@src()), .{ .label = label }));
}
return v_layout.widget();
}
fn renderInput(id_arg: ID.Arg, opts: InputOptions) Widget {
const id = id_arg.id;
const current_text: **[]const u8 = useState(id.push(@src()), *[]const u8, ...initializer);
const input_key = textInputKey(id.push(@src()));
if(input_key.on_commit) |added_text| {
current_text.* = concatString(current_text.*, added_text);
}
var v_layout = VerticalLayout{ .gap = 1 };
v_layout.put(renderText(id.push(@src()), opts.label));
v_layout.put(handleInput(input_key,
renderBorder(
.{.w = 1, .color = .black},
renderText(id.push(@src()), current_text.*),
),
));
return v_layout.widget();
} Render functions are called every frame. Functions like useState() need to return the same pointer every frame, rather than recreating the content, so an id is used to key into a persistent hashmap. Inside of loops, the id has to be keyed so that the same item in an array will always make the same ids.
pushFunction was a bad example, but
Yeah. Essentially, any time I need to loop over anything, a new id is required that should shadow the parent id, as using the parent id is usually an error |
@pfgithub Righy-ho, that all makes a lot more sense, thanks for taking the time to clear that up; that certainly is a use-case I would like for this proposal to cater to. |
This was proposed and rejected in #594, however, I think the conversation (in particular my own comments) did not adequately address the proposal, so I want to say thank you to @InKryption for putting effort into making this a high quality, concrete proposal. However, I do think the decision to reject this is the best design decision for Zig, and I am confident enough in this assessment to close this issue. The value this provides is ability to prevent someone from accidentally using a variable that has been deleted. However, let's consider the use case in which this happens: it's when someone is looking at a long function, and may not be aware of the whole thing. So the person reading the code only sees subsets of the function's body while making edits. In such case, declaring a variable with a conflicting name is already a compile error, so that can't be a problem. It would just be copy-pasting code or making a typo that this would catch. In practice I have found it better to name things properly, and then everything works out fine. I can address all 3 motivating use cases this way: pub fn calculate(starting_x: i32) i32 {
var x = starting_x;
x += 50;
return x;
} With the parameter named this way, it's not really possible to make this mistake. {
var idx: usize = 0;
while (idx < N) : (idx += 1) {
// ...
}
} We already have scopes to solve this problem. Often there is more than just the index variable that should be limited in scope, and the programmer is encouraged to put all relevant declarations in the inner scope. I can foresee var mutable_device_features: c.VkPhysicalDeviceFeatures = undefined;
_ = c.vkGetPhysicalDeviceFeatures(device, &mutable_device_features); In this example, I don't think anyone is going to type pub fn alloc(self: *Allocator) void {
const self = @fieldParentPtr(CustomAllocator, "allocator", @hide(self)); If this can happen anywhere in the function then we would have the situation where you could be looking at a variable declaration and still not know what the type of that variable is when used elsewhere in the function. Additionally, A generalization of this feature which retains language simplicity would be allowing redeclaration of the same name, like Rust does. However, I veto this on the basis that a person reading code has fewer guarantees about what names mean. The fact that when you read zig code right now, an identifier always means the same thing even if you haven't seen everywhere the identifier is used in the function, is invaluable. I appreciate the discussion on this topic, however, I am confident in the direction of the language in this case. |
This idea was originally brought to my attention by @marler8997 here: #9696 (comment).
The basic premise would be that using
@hide
on an identifier would disallow further use of it, and return its value, like so:I believe this could be of some use, as besides remedying the original concern of the issue it was mentioned in, it could also potentially solve a few current pain points, like the scope of temporary index variables in while loops:
which is a lot easier to write, and is much more explicit in stating that
idx
should specifically only be used within the while loop, which would be especially beneficial in longer sections of code that are being refactored, seeing as where many might omit wrapping the above in its own block out of negligence/laziness, writing_ = @hide(idx);
is but a few keystrokes, and gives the same security that making the block would do.Another example where this could remedy some pains would be when working with functions that use out-parameters, like this:
Other considerations:
const device_features = @hide(device_features);
.For sanity's sake, I think that if this were to be allowed, it should be limited explicitly to cases where the hidden identifier's value is being assigned to an identifier of the same name, making it illegal to do something like:
That said, this could be a way to resolve the issues #498 attempts to address, without adding other special syntax; although to be fair, it would still technically be a special case.
EDIT: The following was addressed by @SpexGuy, and I now consider it to be irrelevant. Focus should remain on how this will affect the language and best practices.
Iirc, proving a variable is not referenced by pointer is usually undecidable, meaning this would be a pretty uncommon, but I will put this here anyway:
the other, more secondary, thought I had about this was optimization. Now, I'm nowhere near knowledgeable enough about compilers to make any substantive arguments or statements regarding this, but in my layman's mind, it would make sense that by hiding an identifier, you would make it easier for the compiler to prove that a mutable variable is not modified after a certain point, meaning that when doing
const new = @hide(old);
, the compiler could then reuse the memory location of "old" for "new" instead of copying the memory.On that note, being that I am no expert in compilers, I also have no idea how complex it would be to "remove" an identifier from scope, so feedback or explanation from anyone who understands more about how this might be done, or how it would be hard to do, would be appreciated.
The text was updated successfully, but these errors were encountered: