A previous discussion on here got me thinking about Pass-By-Value errors, and how insidious they can be; it seemed like an easy mistake to make, but potentially a very difficult one to debug. I came up with an idea to use zig’s @typeInfo inbuilt to recursively search through namespaces for functions that pass structs by value, and then check to see whether those structs contain a particular tag declaring them as “pass-by-reference only”.
const std = @import("std");
/// To subscribe a struct to reference-only linting, include
/// `const PASS_TYPE : PassType = .referenceOnly;`
/// in it. Note that this only works on functions marked `pub`
/// in namespaces subscribed in the comptime block.
const PassType = enum {
referenceOnly,
valueAllowed
};
comptime {
var typelist : []const type = &[_]type{@This()};
var i = 0;
while(i < typelist.len) {
defer i += 1;
const t = typelist[i];
const info = @typeInfo(t);
// We only care about structs here, although this could be expanded to enums
switch (info) { .@"struct" => |structinfo| {
for(structinfo.decls) |d| {
const decl = @field(t, d.name);
const decinfo = @typeInfo(@TypeOf(decl));
switch (decinfo) {
// Lint function. This only works on functions marked `pub`, unfortunately
.@"fn" => |f| {
for(f.params) |p| {
const paramtype = if(p.type) |ty| @typeInfo(ty) else continue;
const pt = p.type.?;
// We only care about structs here
switch (paramtype) { .@"struct" => {
if(@hasDecl(pt, "PASS_TYPE")
and @TypeOf(@field(pt, "PASS_TYPE")) == PassType
and @as(PassType, @field(pt, "PASS_TYPE")) == PassType.referenceOnly) {
const errstr = "In function '" ++ @typeName(t) ++ "." ++ d.name ++ "', took parameter of type '"
++ @typeName(pt) ++ "' by value, where field 'PASS_TYPE' specifies 'referenceOnly'.";
@compileLog(errstr);
}}, else => {}}
}},
// Add declared type to type list, allowing recursive linting
// Also only works on structs marked `pub`
// Additionally, if you have fields marked `pub`, e.g.
// ```pub const std = @import("std");```
// Then these will also be searched
.type => {
typelist = addType(typelist, decl);
}, else => {}}
}}, else => {}}
}
}
fn addType(comptime list : []const type, comptime Element : type) []const type {
for(list) |T| {
if(T == Element)
return list;
}
return list ++ [1]type{Element};
}
pub const A = struct {
const PASS_TYPE : PassType = .referenceOnly;
bigdata : [10000]u8,
// Note that this example optimises away the @memcpy, but we shouldn't assume it will do that
pub fn cool(a : A, n : usize) void {
std.debug.print("{}", .{a.bigdata[n]});
}
};
pub fn main() !void {
var ind : usize = undefined;
if(std.os.argv.len > 1) {
const endind = std.mem.indexOfSentinel(u8, '\x00', std.os.argv[1]);
ind = try std.fmt.parseInt(usize, std.os.argv[1][0..endind], 0);
} else {return;}
const a : A = A{.bigdata = undefined};
a.cool(ind);
}
(Please forgive the ugly switches, I wanted to avoid having ludicrous amounts of indentation)
The result was only moderately successful. In this demo, it’s able to detect the pass-by-value in the function “A.cool”, but in many other cases, I wouldn’t be able to do the same. @typeInfo only provides you access to declarations marked pub, so if pub const A was instead written const A or pub fn cool was fn cool, then the problem would never be picked up.
On the other hand, supposing @typeInfo could access declarations not marked pub, then the comptime block would comb through everything in std, which is undesirable - in fact, it causes a compilation error trying to access namespaces which it shouldn’t on my machine. The issue here is that there’s no way for me to distinguish between std, an imported struct, and A, which is defined within this file.
I would be keen to do things like this in my own projects to write my own safety rules, but it’s not really usable in its present form. Please share any ideas on how it could be improved or made viable.
It would certainly be helpful to me if we could perform more robust comptime analysis like this, but perhaps that’s outside the scope of what comptime is supposed to achieve. What do others think?