Var not mutable?

I am hitting a question during some code golfing.
Let’s say I have this:

var a =
\\hello
\\world
;

Why can’t I change one character of this variable like:

a[3]='x'; // error: cannot assign to constant

I’m guessing that declaration creates a variable pointer to a static string.
You can’t override the elements because they’re in the text section of the executable

1 Like

it is mutable, but there are multiple layers of mutability because "asdf" creates an immutable pointer.

You can a = "another string",
but you cannot modify the contents of that string, as it is not mutable.

4 Likes

So i would have to transform the thing to a normal array of bytes to achieve mutability i guess.

3 Likes

(post deleted by author)

Yup, you can do that easily as var x = "asdrf".*;
or

    var x =
     \\asd
     .*;
13 Likes

Hah, @vulpesx beat me to it. Here is a complete executable example:

pub fn main() void {
    var a =
        \\hello
        \\world
        .*
    ;
    a[3] = 'x';

    @import("std").debug.print("{s}\n", .{a});
}
> zig run var-string.zig
helxo
world
5 Likes

I sometimes wonder if different keywords would make sense for “not re-assignable” and “immutable”. E.g. ‘const’ would only mean that the item cannot be assigned another value, and ‘immutable’ means that value cannot be mutated.

I didn’t really think about the difference until encountering the JS/TS behaviour where const only prevents assigning, but allows mutation.

E.g. the following variations:

const mutable bla: Bla =...; // content can be modified, bla cannot be reassigned
const immutable bla: Bla = ...; // same as current const
var mutable bla: Bla = ...; // same as current var
var immutable bla: Bla = ...; // bla can be reassigned, but content is 'readonly'

…that makes const mutable no longer a comptime value though.

Or maybe readonly instead of immutable, sounds less ‘highbrow’ :wink:

1 Like

I think status quo Zig semantics for this are basically the same as JS/TS (or Java, for the matter), but confusion may stem from the fact that those languages only support references to non-primitive objects, i.e. the variable itself always represents a pointer, while in Zig (and C and C++ and C#…) the variable itself may also represent the raw object value, which is not possible in JS/TS.

When I was starting out with Zig, this confused me as well (even though I was already intimately familiar with C), but once it clicked, the semantics are simple, really. The var and const keywords always relate only to the variable itself, but not what the variable may point to when it’s a pointer. Whatever that variable ultimately points to is governed by the pointer type. mutable and immutable would be redundant.

4 Likes

Hmm right, good point!

A const bla = { a: 1, b: 2, c: 3 }; in JS/TS is more like a const bla: *Bla = try heapAlloc(allocator, Bla, .{ .a = 1, .b = 2 }); in Zig.

E.g. a const pointer to variable data on the heap…

2 Likes

I’d say it is a bug that this works, because it is a foot gun. If there were 200 lines between the declaration and the modification of the content, and this would be a function called a second time, you would be quite surprised that the 4th byte is not an l.

Precisely! It’s always been a bit of a pet peeve of mine that languages like JS, TS, and Java hide references syntactically like this. It’s really harsh on beginners’ understanding of these semantics (seen that at my university where Java was unfortunately the primary language taught).

2 Likes

To prevent these problems, I prefer to “lock in” variables once all required modifications are done by limiting the var scope to a small block like this:

pub fn main() void {
    const a = blk: {
        var a =
            \\hello
            \\world
            .*
        ;
        // `a` is mutable here and we can make changes
        a[3] = 'x';
        break :blk a;
    };

    // `a` is now `const`, you can't hurt it (and it can't hurt you)

    // ... 200 lines of code ...

    @import("std").debug.print("{s}\n", .{a});
}

EDIT: Clarifying comments

7 Likes

I haven’t tested this, but my intuition expected that a shadows a here.

It actually doesn’t, because in function scope, a variable only exists after its declaration (which includes the initializer expression).

It would however shadow itself if it were in struct scope (i.e. a “global”), because declaration order does not matter there, so the variable exists “everywhere”.

1 Like

Ahh, yes. Thank you. Makes sense. Block is the process of initialization itself here, thus no shadowing.

1 Like

I never knew that was a way. Here I’ve been writing my {'v', 'a', 'r', 'i', 'a', 'b', 'l', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 's'} like a fool.

3 Likes

Simply put, the type of a is *const [_:0]u8
Explicitly writing out the type instead of relying on automatic type inference can help alleviate such confusion.

    // > String literals are constant single-item Pointers to null-terminated
    // > byte arrays. -- https://ziglang.org/documentation/0.16.0/#String-Literals-and-Unicode-Code-Point-Literals
    const A = Pointer{
        // String literals are const
        .is_const = true,
        // single-item Pointers
        .size = Pointer.Size.one,
        // to
        .child = Array{
            // null-terminate
            .sentinel_ptr = null,
            // byte arrays
            .child = u8,
            // Array's length is known in compile-time
            .len = 11,
        },
    };
    var a =
        \\hello
        \\world
    ;

Since a is a pointer so we can deference it to get the pointer child (or pointee which the pointer points to) to get the byte array. Fine, we can operate the array way what we want.

Neither I knew about this.
I find it strange to dereference a const to get a static initialized value, but it works.