Pub, files, and container types

Ok, I find that fairly astonishing actually. I’ve written a lot of Zig at this point, and always on the premise that namespaces only expose pub members.

I suppose that would make it easier to test things, I definitely have a function here or there which is only public so it can be tested.

I’m not sure that I like it, frankly. But hey, TIL.

3 Likes

I think it can potentially be useful when you aren’t testing the public interface but instead some internal invariants across a bunch of different types that are all within the same file, because then it gives you a way to write the test without forcing you to make things public that are only used for that testing.

I think it also can help with making some of the autodocs more readable, by allowing you to keep some things hidden from the publicly documented interface.

I haven’t looked into the pros/cons deeply, but I kind of like this behavior.

4 Likes

I can see the upsides. Instead of public, private, protected, it’s just pub, and no keyword is protected.

What I don’t like is that it’s less simple, because you can’t just say “files are structs”, there’s more to it than that.

There are at least two ways in which files are not just structs: the fact that sub-scopes can see inside each other, and the fact that test blocks aren’t allowed inside a struct, just a file. Are there others?

I wonder if allowing test blocks inside containers would cover enough of the benefits of the way pub works to make it practical to simplify both divergences.

“Files are just structs” would be a nice statement to be able to make, as opposed to “files are structs, except…”.

Zig doesn’t do private fields, so on a philosophical level I don’t see a great problem with this making a few more functions or declarations visible outside the file. If someone wants functionality like that to not count for SemVer, for instance, just document that it isn’t considered stable API.

3 Likes

Visibility is often an empty promise.
private is never really really private. How many hacks I remember to get access to a private field…

The only Zig-danger of everything public is that everything is public.
No black boxes (except when we use non pub vars inside a file).

Regarding your remark “this just works” I don’t understand the astonishment.
That example is totally local, so that definitely should work!

4 Likes

Well that’s the thing. I thought the simple rule was the rule: if it isn’t pub, a declaration can’t be seen outside of a container.

But that’s wrong. pub makes declarations visible outside the file, so files are the only containers it applies to.

5 Likes

In my view it would be insane if otherwise :slight_smile:

Why though?

Everything within one file is visible to everything in that same file.

At least: if we are understanding eachother correctly…

That’s the ‘what’, I want to hear about the ‘why’.

Per the counterfactual, anything without pub is container local, instead of file local, and all containers can have test blocks.

This I’m told is insane. Why is it insane?

2 Likes

pub means “reachable from other files”, that’s the simple rule.
In my naive approach (experimenting in my main.zig) I never had questions there.

I am not yet into tests. So I have no answers there.
For the why I have to refer you to Andrew and his collegues.

I have old experience with the C# language and even Delphi language.
In C# private is container private. Nothing can reach it except the class itself.
In Delphi private is file private.
Somewhere in history then they invented strict private which finally was container private.

So there we have

public
published
protected
private
strict protected
strict private

which drove me mad sometimes. Aargh I can’t reach that variable. I need it.

I called it insane, because I am used to that kind of visibility.
I actually forgot how it is in Rust.
And in Zig it was exactly the same except that we have only pub or nothing.
(Nice and simple).

Hot take here: we should just remove visibility. Make everything public. Python is doing fine without them.
Zig already made fields public, and Andrew’s explanation for it made total sense (can’t find it now), but the explanation of why functions should be different did not. We should go all the way.
Just now, in the other topic, I had to do a ridiculous dance to get a few function pointers.
Mark functions as private just as a suggestion for reading purposes and for tooling. Don’t enforce them.

3 Likes

Hahaha yes! Very funny. Reminds me of my private field hack days.

1 Like

I have no problem with this. I would prefer either of all-public and container-scoped pub to what we have.

The current behavior is also ok, except for the part where you can’t actually say “files are structs” without enumerating the exceptions. That has some practical shortcomings as well as pedagogical ones.

If files really are structs, then you can:

  • Make a file into a struct by wrapping it in struct { ... }
  • Make a struct into a file by unwrapping it
    • This is mostly not a problem now, but keeping tests inside the struct would make it easier to do
  • Move any struct into another file, import it by the same name, and everything would still work without needing to tweak publicity.

Since I was today years old when I found out that pub is file-local, not container-local, everything I’ve written is in the counterfactual Zig where that’s how pub works. It’s fine, I have had no problems with it whatsoever.

I wouldn’t balk if there was no privacy in namespaces at all, but it matters enough to other people that they’d start doing (like Python) the thing were private functions get a leading underscore, if it weren’t available.

There’s always the case that either of those would be a big change for little benefit, and it’s true that fine-tuning visibility is not a high stakes question for the language. But it would make Zig simpler, and for me that makes it worth considering.

3 Likes

Tests are allowed inside structs:

const SomeStruct = struct {
    test "sometest" {
        try std.testing.expect(1 == 0);
    }
};

As long as that struct is referenced in the tests, it should run that test an fail.

After a fresh zig init in 0.14.0, I changed src/root.zig to

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

pub export fn add(a: i32, b: i32) i32 {
    // Have to reference the struct so that the test runner will pick up the tests
    const hidden = Hidden{};
    _ = hidden;

    return a + b;
}

const Hidden = struct {
    test "hidden" {
        std.debug.print("This is a hidden test\n", .{});
        try testing.expect(1 == 11);
    }
};

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}

And running zig build test shows the failing test.

5 Likes

That is utterly baffling to me. This I distinctly remember trying to do, right in my first Zig project, and being stymied.

Clearly what was wrong was not the nested tests. Always have to be careful when debugging to distinguish between happenstance and essence.

What can I say, 0 for 2 on some very basic Zig questions. I even checked if it was a recent change, but no, this substantially predates my involvement with the language. A learning experience!

I think this considerably strengthens the case for making declaration publicity work the same for files and any other container. It looks like that’s literally the only way in which they differ (happy to hear a counterexample!), so it would be pretty nice to take that number down to zero.

4 Likes

In fact I was contemplating: are files structs? No. They are - in my view - definitely not.
You cannot make multiple instances of them, which is the basic behaviour of structs.
Or am I hallucinating?

const MyType = @import("MyTypez.zig");

const instance1 = MyType{};
const instance2 = MyType{};

Files are structs. You can declare their fields at top-level.

//! This is a file
field: u32 = 0,
7 Likes

Indeed. I was wrong (read hallucinating).

Since this thread has been revived, I wanted to add a few thoughts I’ve been ruminating on.

I think we are conflating 2 characteristics here: structs and compilation units. The reason this distinction is important is that pub is related to the compilation unit, not the container. pub exposes namespace members to the outside. Structs are containers for variables and procedures.

In this case, files have both these characteristics. A file is a struct. A file is a compilation unit. However, a struct is not a compilation unit.

So a variable or procedure can be accessed with in a compilation unit regardless of the visibility. Outside can only reference namespaces/container/procedures that have been declared pub.

This is why the behavior that initiated the conversation works. As long as a test is in the same file, it can reference all namespaces in the file regardless of the visibility modifier. The same is for namespaces/structs inside the file. They can access any variable or method inside the file regardless of visibility.

So yes, that is the only way they differ but I think that it is fundamentaly a different concern, at least for how the compiler currently works. I think mnemnion’s point still stands in that there may be a conversation about changing pub behavior from the compilation unit to the container. I personally like the way it is, the other is what other languages (e.g. Rust) do and that often leads to making things public that are really implementation details for the sake of allowing a sibling to operate on another sibling struct.

Edit: Per Sze’s correction, I have updated the comment to try to prevent misinformation

I thought that with Zig a compilation-unit corresponds with an artifact like an exe or a library.

In C a file is synonymous with a compilation unit, because they can be converted into object files separately, which then get combined through the linker.

So it doesn’t seem right to me, to speak of files as compilation units, within the context of Zig. Otherwise I would expect to be able to compile a bunch of error returning functions separately from another, to some sort of new object-file-format and combine them later, adapting the error-integer values while linking.

1 Like