Looking for guardrails for "hidden" pointers in return by value constructors

I started working on a hobby project in Zig recently, and ran into a bug that took me by surprise.

I found the bug, but I’m wondering if there are any best practices or tricks I could employ to make this kind of bug less likely in the future. Here’s the bug I introduced, in an abbreviated version of the code.

const std = @import("std");

const State = struct {
  pcg: std.rand.Pcg,
  rand: std.rand.Random,

  // ...

  pub fn Create() State {
    var pcg = std.rand.Pcg.init(@intCast(u64, std.time.timestamp()));
    var state = State{
      .pcg = pcg,
      .rand = pcg.random(),
    };

    return state;
  }

  pub fn CreateId(self: *State) u16 {
    return self.rand.uintAtMost(u16, std.math.maxInt(u16));
  }
};

pub fn main() void {
  var state: State = State.Create();

  var i: i32 = 0;
  std.debug.print("vals:\n", .{});
  while (i < 100) : (i += 1)
  {
    // This is going to give us the same random value 100 times, 
    //    because state.pcg is not being updated.
    std.debug.print(":{x}\n", .{state.CreateId()});
  }
}

So the problem here as I understand it is that std.Pcg.random() takes it’s self parameter as a pointer, which will of course go out of scope when this function exists. For that matter, in this version it’s pointing at an item on the stack which isn’t even in the State structure. Which led me first to this wrong correction:

pub fn CreateRevised() State {
    var state = State{
      .pcg = std.rand.Pcg.init(@intCast(u64, std.time.timestamp())),
      .rand = undefined,
    };
    state.rand = state.pcg.random();
    return state;
}

After which I got to the correct (I think) version:

  pub fn Init(state: *State) void {
    state.* = State{
      .pcg = std.rand.Pcg.init(@intCast(u64, std.time.timestamp())),
      .rand = undefined,
    };
    state.rand = state.pcg.random();
}

I do like the uniform access syntax for struct and pointers to structs, but this feels like something of a foot-gun. The std documentation shows that pcg.random() takes the self parameter as a pointer, but there’s nothing in the usage code that indicates it’s being passed by reference. It’s not as though this is an unreasonable thing for the code to do, but because it’s referencing data that goes out of scope, tracking down the source of an error caused by this seems like it could be very unpleasant.

I’m pretty new to zig, are there steps I can take in my usage code to detect or mitigate this?

There’s nothing that detects this currently, but detecting it is planned in some form AFAIK. The relevant issues are:

1 Like

Well that answers my question. I’m glad that this is being worked.

Thanks for the information.