Static Local Variables surprised me, what am I missing to make them intuitive?

I was reading through the language reference and did a double take at the example for Static Local Variables

const std = @import("std");
const expect = std.testing.expect;

test "static local variable" {
    try expect(foo() == 1235);
    try expect(foo() == 1236);
}

fn foo() i32 {
    const S = struct {
        var x: i32 = 1234;
    };
    S.x += 1;
    return S.x;
}

I don’t understand how or why S is shared across invocations of foo? Reading the example code I would have thought that every invocation creates a new instance of the struct S? That S would be destroyed at the end of the function scope as it would be part of foo’s stack frame?

Please explain it like you’re explaining it to someone that has been writing Typescript for the last 9 years, python before that, and a career spent trying to forget everything I learned about java :joy::sweat_smile:

I’m really hungry for a deep, bottom-up, understanding here – I’m learning zig because, amongst other things, I want full control over memory, and this example breaks my current mental model of stack allocation (which is pretty exciting tbh :grin:).

Here’s a theory that is ~probably~ definitely wrong: since a test declaration is effectively a function, maybe S is hoisted up to the test declaration’s stack frame? I add created two test declarations to test this, and found that the state is shared across test declarations. Hmm :thinking: Yeah my current mental model is swiss cheese :sweat_smile:

Thank you!!

Because it is a global variable. It is global because it is declared in the root of a type. The type being declared in a local function scope is irrelevant.

Btw files are implicit struct’s, so it is literally identical to declaring a global in the root of a file, as far as the language is concerned.

Right! So this is almost identical to

const S = struct {
    var x: i32 = 1234;
};

fn foo() i32 {
    S.x += 1;
    return S.x;
}

– the difference being I guess that in the former example, S wouldn’t exist as a type before foo is invoked?

No, it is identical. In both cases, S exists as a type, and could even be used outside the function if you could access it.

The only difference is where it is defined, and by extension whether you can access it elsewhere.


also, in your post you seem to have thought x was a field, at least the way you described how you expected it to work would be that of a field in zig. x is not a field in this case, it is a global declaration.

4 Likes

Ah! Thank you, so it’s kind of like a hidden type?

I think I found the hole in my mental model, I thought “what if I make another instance of S?“, so I tried

test "instance of S?" {
    const y: S = .{ .x = 4 };
    try expectEqual(y.x, 4);
    try expectEqual(bar(y), 5);
}
fn bar(b: S) i32 {
    b.x += 1;
    return b.x;
}

and that’s when I realized xisn’t a field on the struct, it’s a declared variable inside the struct, I think?

ah, you spotted the same flaw in my thinking before I posted :sweat_smile: thanks!

1 Like

I did edit in an explanation of your misunderstanding:

the way you would define a field is like this:

const S = struct {
    // no const, no var
    x: i32,
};

if its a field then you’d have to make an instance in order to access x which you didnt do until now.
so unless you used a global instance, it would not persist across invokations.

1 Like

and that makes the whole “a module is just an implicit struct“ concept really click! if I can declare a variable inside a file, then I can declare a variable inside a struct in exactly the same way, with the same semantics :grin:

now that I finally understand that, I can see how useful that is going to be!

what you might still be missing on that concept, is you can also decalre fields in a file, and instantiate it like any other struct.

see std.Io.Reader as the first real world example that came to mind.
but also a simpler example:

x: i32,
// @This() allows you to refer to a type when you cant refer to its name
// the type still has a name, in this case it will be the files name
// also useful for generic types
const zero: @This() = .{ .x = 2 };
// you could also make an alias
// It's better practice to use the real name of the type
// in this case the file name, but thats not enforced.
const Self = @This();
5 Likes

:exploding_head: that’s awesome! Ohhh, that makes so much sense of the Ghostty code I was reading the other week :grin:

This is a very minds breaking trick of zig, when migrating from C world (in my case). But it helps to write less code, despite the fact that zig discourages such practices. :slight_smile: Check out zig sources, they use it everywhere, making it state of practice in zig programming (at least imho).

1 Like

On the contrary - where do you get that idea?

EDIT: I must be missing your meaning, I guess, as you seem to say the opposite right after, I see.