Questions about mutability, lambdas and many-item pointers

Hello there.

While I’m still learning about the language, I collected some questions I were bothering me past these days. I’m putting them in one thread to not spam more.

  1. It’s known that using of unmodified vars is prohibited.
    Let’s consider this use case:

    test "var pointer test" {
      var test_var: i32 = 1;
      var test_var2: i32 = 2;
    
      var test_ptr: *i32 = &test_var;
      test_ptr = &test_var2;
    }
    

    Here we assign two var variables to both var and const pointer. Zig should enforce the var assignment usage during the variable lifetime. Here, however, it skips the rule, and program passes the check. Same works for const test_ptr.
    If we replace pointer assignment with variable skip use, we get correct surface check for error of both var variable and var pointer:

    test "var pointer test" {
      var test_var: i32 = 1;
      var test_var2: i32 = 2; //error: local variable never mutated
    
      var test_ptr: *i32 = &test_var; //error: local variable never mutated
      // test_ptr = &test_var2; 
      _ = test_var2;
      _ = test_ptr;
    }
    

    Therefore, I have a question:

    Is it necessary to enforce mutability of a variable through a pointer?
    If yes, how hard is it to implement and what bottlenecks are currently present for development of this feature?
    Or this behavior is simply working as intended?

    Related issue: https://codeberg.org/ziglang/zig/issues/31049

  2. At one of the Github discussions I noticed the mention of lambda implementation, and how it’s pure usage is forbidden in Zig due to it being not explicit enough. Here is my, quite ambiguous, take (from inspiration of possible implementation that also was mentioned in the thread):

    const print = struct {
        fn self(comptime fmt: []const u8, args: anytype) void {
            return std.debug.print(fmt, args);
        }
    }.self;
    

    Here the question:

    Is this construction viable for a real use case (and if yes, what is the proper example), or I should avoid writing my code like this due to there existing more optimal solutions?

  3. I still can’t wrap my head around the concept of many-item pointers. I know they exist so we can point to an unknown amount of items in a buffer at runtime, But same we can workaround this limitation by using simple pointers.

    How many-item pointers were created at the first place and what are use cases where we absolutely can’t do without them? Why can’t we chain these pointers?

    Related question: Pointer Arithmetic / Offsets

Welcome to Ziggit!

1

Currently, taking the pointer of a variable (with &) is enough to not get the error, even if the variable is not actually modified though this pointer.

One reason for this is that this check happens in an early stage of the compiler, where the compiler does not yet know much about the behavior of other functions. See also this topic for a similar question.

But even if the check would do more complex analysation of the code, it’s impossible to say for every case if a variable could get modified, or if it will never be modified. For example:

const tm: TuringMachine = ...;

pub fn main() void {
    var x = false;
    tm.run();
    x = true;
}

The variable will not be modified if the turing machine does not halt. But it’s impossible to know if that’s the case or not.

2

You can usually just define the labda function seperately as a normal function. I think the exception is when the labda function uses comptime parameters of the outer function, though I can’t currently think of a good example where this is the case. So there may be use cases where this is a good solution, but it’s rare.

3

In C, a pointer is used both to point to a single item and to point to an array. In zig, there are multiple pointer types for this. So it’s at least needed for interaction with C, because C functions may expect multi-item pointers.

In zig, you often use slices, which is a multi-item pointer with a length.

There are some cases though where it makes sense to use a multi-item pointer instead of a slice, usually because the length is already stored elsewhere. For example:

const Image = struct {
    width: usize,
    height: usize,
    data: [*]Color,
};

data could also be a slice, but the length is already implied by width * height, so it’s not needed to store it.

3 Likes

I think what’s matter is the intent of the modification. If we spawn a pool of threads we should take in mind of a mechanism to free the allocated memory regardless of the program state. Same as bounds check on an array.

If a TuringMachine in this example would be a simple infinite while loop with no exit state, the compiler should be able to recognise that program is never finished and, thus, x is never supposed to be modified. Another question, how hard is to implement it.

In general it is undecidable, that is why some programming languages want to constraint your program enough, so that an easier version of the problem becomes decidable. But that means that you no longer can express a bunch of programs you could before, because they can’t be proven to be correct, through an algorithm.

I think it would be weird if some arbitrary pattern matched programs/turing-machines were recognized and would lead to a compile error, while others which just happen to be unknown variations of that which are allowed.

That is why I find Zig’s rules better, more consistency, I think this is also important if the turing-machine can be swapped out based on comptime conditionals, if some caused errors while others didn’t you would have trouble switching them out, for example based on build options.

6 Likes

Not sure what you mean by a “simple pointer”? Do you mean a C-like pointer?

Zig has four main pointer types:

  • A single item pointer: e.g. *i32 for example. It points to one and only one of the target type.
  • A slice: e.g. []i32. It points to a run-time known number of the target type.
  • An array e.g. `[5]i32. It points to a compile-time known number of the target type.
  • A many-item-pointer: e.g. [*]i32. It points to an unknown number of the target type.

The first three are things that C pointers are used for in C, but they have more constraints on them. That allows the compiler to understand them better. For example, two single-item-pointers with different values can’t alias to the same memory (I’m 90% sure of that). That can be useful knowledge to the compiler.

Many-item-pointers are the “catch-all” type once you’ve exhausted the others. They’re your last choice because they have no constraints.

1 Like

I wouldn’t say that an array in Zig is a type of pointer.

1 Like

I tend to agree, but it’s a related type.