No field or member function named `init` in `scanner`

So I’m trying to build a scanner and I’m getting this error where the compiler is unable to find the init method in the scanner struct.

scanner.zig:

const Scanner = struct {
    const Self = @This();
    var start: usize = 0;
    var current: usize = 0;
    var line: usize = 1;

    var source: []u8 = undefined;
    var tokens: std.ArrayList(Token.Token) = undefined;
    var keywords: std.StringArrayHashMap(Token.Token_Type) = undefined;

    pub fn init(self: *Self, allocator: *std.mem.Allocator, src: []const u8) void {
        self.source = src;
        self.tokens = std.ArrayList(Token.Token).init(allocator);
        self.keywords.init(allocator);
        self.keywords.put("and", Token.Token_Type.AND);
        ...
    }

Here’s my function in main where I’m trying to call it:

fn run(source: []const u8) !void {
    const stdout = std.io.getStdOut().writer();

    var scanner = Scanner{};
    try scanner.init(allocator, source);
    defer scanner.deinit();

    var tokens = try scanner.scan_tokens();
    defer tokens.deinit();

    for (tokens.toOwnedSlice()) |token| {
        const token_str = try token.to_string(allocator);
        defer allocator.free(token_str);
        try stdout.print("{s}\n", .{token_str});
    }
}

I’ve also tried using &allocator since the argument is a pointer, but that doesn’t seem to work either. I’ve looked at the different methods for ArrayList and StringArrayHashmap and they don’t possibly return an error so I’m pretty sure it’s not an issue with a missing try and/or !

Edit: It actually looks like using put does possibly return an error, so I put a try next to them. However I am still producing the error.

Can’t reproduce your stated error, i.e. this compiles:

const std = @import("std");

const Scanner = struct {
    const Self = @This();
    var start: usize = 0;
    var current: usize = 0;
    var line: usize = 1;

    pub fn init(self: *Self, allocator: std.mem.Allocator, src: []const u8) void {
        _ = self;
        _ = allocator;
        _ = src;
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    var scanner = Scanner{};
    scanner.init(allocator, "foo");
}

but your code probably doesn’t match your intentions, since your Scanner struct has no fields, and instead start/current/line/etc are declarations of the Scanner struct itself (i.e. the way you have it, Scanner.start is effectively a global, and scanner.start doesn’t exist)

You probably want this instead:

const Scanner = struct {
    start: usize = 0,
    current: usize = 0,
    line: usize = 1,

    // note: default initializing to undefined is a footgun waiting to happen
    source: []u8,
    tokens: std.ArrayList(Token.Token),

Typically init functions that don’t need to allocate the instance on the heap will return an instance of the struct, so:

    pub fn init(allocator: std.mem.Allocator, src: []const u8) Scanner {
        return .{
            .source = src,
            .tokens = std.ArrayList(Token.Token).init(allocator),
        };
    }

Don’t take pointers to the Allocator interface. You might see old examples that take a pointer, but this changed ~3 years ago: Allocgate by leecannon · Pull Request #10055 · ziglang/zig · GitHub

Also relevant:

For keywords, you probably want a std.StaticStringMap, keep it a declaration rather than a field, and then initialize it at compile time:

const keywords = std.StaticStringMap(Token.TokenType).initComptime(.{
    .{ "and", .AND },
    // etc
});

(this is exactly what the Zig tokenizer does)

2 Likes

Thanks for you reply. It looks like I actually needed to mark the struct its-self with pub and then do var scanner = Scanner.Scanner

However I have a new issue to deal with now.
I’ve changed my declaration of the struct to

pub const Scanner = struct {
    const Self = @This();
    start: usize = 0,
    current: usize = 0,
    line: usize = 1,

    source: []u8,
    tokens: std.ArrayList(Token.Token),
    keywords: std.StringArrayHashMap(Token.Token_Type),
    ...

and now var scanner = Scanner.Scanner{ }; is asking for the missing struct fields. How should I proceed?
I’m not sure where to put StaticStringMap in place of StringArrayHashMap

EDIT: I’ve changed my declaration as such:

pub const Scanner = struct {
    const Self = @This();
    start: usize = 0,
    current: usize = 0,
    line: usize = 1,

    source: []u8,
    tokens: std.ArrayList(Token.Token) = undefined,
    var keywords: std.StaticStringMap(Token.Token_Type, 16) = std.StaticStringMap(Token.Token_Type, 16).init(.{
        .{ "and", Token.Token_Type.AND },
        .{ "class", Token.Token_Type.CLASS },
        .{ "else", Token.Token_Type.ELSE },
        .{ "false", Token.Token_Type.FALSE },
        .{ "for", Token.Token_Type.FOR },
        .{ "fun", Token.Token_Type.FUN },
        .{ "if", Token.Token_Type.IF },
        .{ "nil", Token.Token_Type.NIL },
        .{ "or", Token.Token_Type.OR },
        .{ "print", Token.Token_Type.PRINT },
        .{ "return", Token.Token_Type.RETURN },
        .{ "super", Token.Token_Type.SUPER },
        .{ "this", Token.Token_Type.THIS },
        .{ "true", Token.Token_Type.TRUE },
        .{ "var", Token.Token_Type.VAR },
        .{ "while", Token.Token_Type.WHILE },
    });

    pub fn init(self: *Self, allocator: std.mem.Allocator, src: []u8) void {
        //self.source = src;
        //self.tokens = std.ArrayList(Token.Token).init(allocator);
        //self.keywords.init(allocator);

        self.source = src;
        self.tokens = std.ArrayList(Token.Token).init(allocator);

Does this look right? (Besides the use of Self)?

pub const Scanner = struct {
    start: usize = 0,
    current: usize = 0,
    line: usize = 1,

    source: []u8,
    tokens: std.ArrayList(Token.Token),
    
    const keywords = std.StaticStringMap(Token.TokenType).initComptime(.{
        .{ "and", .AND },
        // etc
    });

For initialization, you can initialize in place:

var scanner = Scanner.Scanner{
    .source = src,
    .tokens = std.ArrayList(Token.Token).init(allocator),
}

but the convention is to create an init function to make initializing non-trivial structs more convenient:

    pub fn init(allocator: std.mem.Allocator, src: []const u8) Scanner {
        return .{
            .source = src,
            .tokens = std.ArrayList(Token.Token).init(allocator),
        };
    }

so then you’d just call

var scanner = Scanner.Scanner.init(allocator, src);

On the Scanner.Scanner thing, you might be interested in taking advantage of the fact that in Zig, files are structs, so you can create a Scanner.zig like so:

const Scanner = @This();

start: usize = 0,
current: usize = 0,
line: usize = 1,

source: []u8,
tokens: std.ArrayList(Token.Token),

pub fn init(allocator: std.mem.Allocator, src: []const u8) Scanner {
    return .{
        .source = src,
        .tokens = std.ArrayList(Token.Token).init(allocator),
    };
}

and then use it like this:

const Scanner = @import("Scanner.zig");

pub fn main() !void {
    // ...

    var scanner = Scanner.init(allocator, src);
    defer scanner.deinit();
}
1 Like