I see - that’s beginning to make a lot more sense, thank you! So, if I can summarize what I’ve learned so far:
If a function (whose parameter is a pointer type) is passed a real value, Zig will automatically create and pass a pointer to that value, even though the original variable was a real value.
This will be a *const (forbidding modification of the underlying value) - so, if you want to be able to modify the underlying value (as in the next bullet), manually create the pointer rather than relying on this implicit pointerizing.
For a function to mutate one of its arguments, that argument needs to be passed as a pointer rather than as the real value. This is because, if the real value is passed to the function, the function will instead be operating on a copy of the real value rather than on the original real value itself. Passing a pointer allows the function to interact with the actual original real value.
(I think this is true based on testing) An immutable struct’s fields are themselves immutable.
All of that together explains the confusing behaviour I was seeing, thank you!
It was hard to choose a single reply as the “Solution” to this question as it was a lot of insights all building on one another, but Sze pointing out that Zig will automatically create a pointer for passing to a function seemed like the primary insight that helped this click. Hope that’s ok.
I think you are mixing method invocation with for loop payload captures a bit too much, the manual created pointer in for loops is to modify the original elements instead of creating const copies of the elements, so the manual pointer is so that you can create a pointer capture in the for loop, but when you invoke a method on a var you can call a * (non-const) pointer method, without manually creating a pointer.
Method invocation and the resulting type is more about the var/constness of the value and what the method declares as type.
If the value is const than it will be Foo or *const Foo, if the value is a var it will be Foo or *Foo (or *const Foo because you always can call a method that only reads on a var instance / const can be automatically added, but not removed).
So:
var foo:Foo = .{};
foo.bar();
Calls the bar method with *Foo (if it is declared with *Foo).
While:
const foo:Foo = .{};
foo.bar();
Always calls the bar method with Foo or *const Foo (and complains if it is declared with *Foo, because you can’t call a method that mutates on a const instance).
Also note that parameters and payload-captures are immutable, so with a pointer payload-capture you get a pointer that is immutable, that points to something you can mutate. (You can’t change what the pointer points to)
If you have a var s: S you could call any of the three methods like s.mutate(), and the value s will automatically have it’s address taken to be the pointer argument to the function. If you have const s: S then only the first two will be available.
If you only have a pointer (const s: *<const> S) then a method call to the first (s.copy()) implicitly dereferences the pointer to create a copy, and the other two work as normal depending on the constness of the pointer.
And also, function parameters are immutable, so if copy wanted to mutate it’s copy of s, it would have to declare something like var t = s;. Pointer paramaters are also immutable, e.g you can’t change the address it’s pointing to, but a *S parameter can still change the value at that address (not value of the parameter itself).