Cheatsheet: State Machine em Zig

State Machine em Zig

Máquinas de estado são uma das implementações mais elegantes em Zig, graças às tagged unions. O compilador garante que todos os estados e transições sejam tratados exaustivamente via switch, eliminando toda uma classe de bugs comuns em máquinas de estado.

Quando Usar

  • Protocolos de comunicação (HTTP, WebSocket, TCP)
  • Parsing de dados estruturados
  • Fluxo de interface (telas, formulários, wizards)
  • Game states (menu, jogando, pausado, game over)
  • Processos de negócio com estados definidos

Máquina de Estado com Tagged Union

const std = @import("std");

const EstadoConexao = union(enum) {
    desconectado,
    conectando: struct { tentativas: u8 },
    conectado: struct { socket_fd: i32 },
    erro: struct { mensagem: []const u8 },

    pub fn processar(self: *EstadoConexao, evento: Evento) void {
        self.* = switch (self.*) {
            .desconectado => switch (evento) {
                .conectar => .{ .conectando = .{ .tentativas = 1 } },
                else => self.*,
            },
            .conectando => |info| switch (evento) {
                .sucesso => |fd| .{ .conectado = .{ .socket_fd = fd } },
                .falha => |msg| blk: {
                    if (info.tentativas >= 3) {
                        break :blk EstadoConexao{ .erro = .{ .mensagem = msg } };
                    }
                    break :blk EstadoConexao{
                        .conectando = .{ .tentativas = info.tentativas + 1 },
                    };
                },
                else => self.*,
            },
            .conectado => switch (evento) {
                .desconectar => .desconectado,
                .falha => |msg| .{ .erro = .{ .mensagem = msg } },
                else => self.*,
            },
            .erro => switch (evento) {
                .conectar => .{ .conectando = .{ .tentativas = 1 } },
                .resetar => .desconectado,
                else => self.*,
            },
        };
    }

    pub fn descricao(self: EstadoConexao) []const u8 {
        return switch (self) {
            .desconectado => "Desconectado",
            .conectando => "Conectando...",
            .conectado => "Conectado",
            .erro => "Erro",
        };
    }
};

const Evento = union(enum) {
    conectar,
    desconectar,
    sucesso: i32,
    falha: []const u8,
    resetar,
};

pub fn main() void {
    var estado = EstadoConexao.desconectado;

    std.debug.print("Estado: {s}\n", .{estado.descricao()});

    estado.processar(.conectar);
    std.debug.print("Estado: {s}\n", .{estado.descricao()});

    estado.processar(.{ .sucesso = 42 });
    std.debug.print("Estado: {s}\n", .{estado.descricao()});

    estado.processar(.desconectar);
    std.debug.print("Estado: {s}\n", .{estado.descricao()});
}

Parser como Máquina de Estado

const std = @import("std");

const ParserCSV = struct {
    const Estado = enum {
        inicio_campo,
        dentro_campo,
        dentro_aspas,
        aspas_encontrada,
    };

    estado: Estado = .inicio_campo,
    campos: std.ArrayList([]const u8),
    inicio_atual: usize = 0,

    pub fn init(allocator: std.mem.Allocator) ParserCSV {
        return .{
            .campos = std.ArrayList([]const u8).init(allocator),
        };
    }

    pub fn deinit(self: *ParserCSV) void {
        self.campos.deinit();
    }

    pub fn parsearLinha(self: *ParserCSV, linha: []const u8) !void {
        self.campos.clearRetainingCapacity();
        self.estado = .inicio_campo;
        self.inicio_atual = 0;

        for (linha, 0..) |ch, i| {
            switch (self.estado) {
                .inicio_campo => switch (ch) {
                    '"' => {
                        self.estado = .dentro_aspas;
                        self.inicio_atual = i + 1;
                    },
                    ',' => try self.campos.append(linha[self.inicio_atual..i]),
                    else => self.estado = .dentro_campo,
                },
                .dentro_campo => switch (ch) {
                    ',' => {
                        try self.campos.append(linha[self.inicio_atual..i]);
                        self.inicio_atual = i + 1;
                        self.estado = .inicio_campo;
                    },
                    else => {},
                },
                .dentro_aspas => switch (ch) {
                    '"' => self.estado = .aspas_encontrada,
                    else => {},
                },
                .aspas_encontrada => switch (ch) {
                    '"' => self.estado = .dentro_aspas,
                    ',' => {
                        try self.campos.append(linha[self.inicio_atual .. i - 1]);
                        self.inicio_atual = i + 1;
                        self.estado = .inicio_campo;
                    },
                    else => self.estado = .dentro_campo,
                },
            }
        }
        try self.campos.append(linha[self.inicio_atual..]);
    }
};

Máquina de Estado Comptime para Protocolos

Quando os estados e transições são conhecidos em compile time, você pode usar comptime para gerar código de despacho otimizado:

const std = @import("std");

// Tabela de transições resolvida em comptime
fn criarTabelaTransicoes(comptime Estados: type, comptime Eventos: type) type {
    const num_estados = @typeInfo(Estados).@"enum".fields.len;
    const num_eventos = @typeInfo(Eventos).@"enum".fields.len;

    return struct {
        tabela: [num_estados][num_eventos]?Estados,

        pub fn init() @This() {
            var t: @This() = .{ .tabela = .{.{null} ** num_eventos} ** num_estados };
            return t;
        }

        pub fn addTransicao(
            self: *@This(),
            de: Estados,
            evento: Eventos,
            para: Estados,
        ) void {
            self.tabela[@intFromEnum(de)][@intFromEnum(evento)] = para;
        }

        pub fn transicionar(self: *const @This(), estado: Estados, evento: Eventos) ?Estados {
            return self.tabela[@intFromEnum(estado)][@intFromEnum(evento)];
        }
    };
}

Esta abordagem é útil para máquinas de estado com muitos estados e transições, onde o switch aninhado ficaria muito longo.

Considerações de Performance

  • Tagged union é a abordagem mais eficiente: o compilador Zig gera um switch com jump table para unions com enum tag. Transições de estado são tipicamente uma instrução de load e um jump — custo mínimo.
  • Estado com dados embutidos: o grande diferencial das tagged unions é poder armazenar dados dentro do estado. conectando: struct { tentativas: u8 } não precisa de alocação separada — os dados vivem dentro da union.
  • Enum simples vs tagged union: se os estados não precisam de dados associados, use um enum simples com switch. É mais leve que uma union completa e o compilador pode representar o enum em um único byte se houver menos de 256 valores.
  • Máquina de estado em hot path: em parsers de alta performance (JSON, HTTP), a máquina de estado é executada para cada byte de entrada. O custo de cada transição deve ser mínimo — prefira enum simples com switch a unions com dados complexos.

Erros Comuns

Não tratar a transição else => self.*: ao adicionar um novo evento ou estado, é fácil esquecer de adicionar as transições correspondentes. Com else => self.*, o compilador não vai alertar sobre casos faltando. Para máquinas críticas, use else => @panic("transição inválida") em debug para detectar estados não tratados.

Estado mutável compartilhado entre transitions: se a função processar chama código externo que pode modificar o estado antes do switch ser avaliado, você pode ter race conditions. Sempre capture o estado atual em uma variável local antes de processar.

Confundir estado e transição: o estado representa onde o sistema está; a transição representa o que aconteceu. Um evento conectar não é um estado — é o evento que causa a transição de desconectado para conectando.

Perguntas Frequentes

Como persistir e restaurar o estado de uma máquina? Com tagged unions, serialize o tag e os dados do estado ativo. Use std.json.stringify se os dados do estado forem serializáveis, ou serialize manualmente para um formato binário compacto. Na restauração, leia o tag e construa o estado correto.

Posso ter ações executadas na entrada/saída de estados? Sim — adicione hooks aoEntrar e aoSair que são chamados pela função processar durante a transição. Guarde o estado anterior, execute aoSair no estado anterior, mude o estado, execute aoEntrar no novo estado.

Como debugar qual transição aconteceu? Adicione logging na função processar: antes e depois da atribuição self.*. Em modo debug, imprima @tagName(estado_anterior), o evento, e @tagName(self.*). Em produção, remova com if (builtin.mode == .Debug).

Quando Evitar

  • Lógica com apenas dois estados (basta um bool)
  • Quando as transições são lineares e nunca voltam
  • Estados que nunca mudam em runtime (use comptime)

Veja Também

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.