Pattern Matching e Regex em Zig — Bibliotecas e Técnicas

Pattern Matching e Regex em Zig — Bibliotecas e Técnicas

O Zig aborda pattern matching de forma pragmática. Embora a biblioteca padrão não inclua um motor de expressões regulares completo (por questões de tamanho e complexidade), o ecossistema oferece múltiplas opções: desde bibliotecas nativas em Zig até bindings para PCRE2. Além disso, as capacidades de comptime do Zig permitem compilar padrões em tempo de compilação para performance máxima.

Busca de Strings na Biblioteca Padrão

O std.mem oferece funções eficientes para busca e manipulação de strings:

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

pub fn main() void {
    const texto = "O Zig é uma linguagem de programação incrível";

    // Buscar substring
    if (mem.indexOf(u8, texto, "linguagem")) |pos| {
        std.debug.print("'linguagem' encontrada na posição {}\n", .{pos});
    }

    // Verificar prefixo e sufixo
    const comeca = mem.startsWith(u8, texto, "O Zig");
    const termina = mem.endsWith(u8, texto, "incrível");
    std.debug.print("Começa com 'O Zig': {}\n", .{comeca});
    std.debug.print("Termina com 'incrível': {}\n", .{termina});

    // Contar ocorrências
    const count = mem.count(u8, texto, "a");
    std.debug.print("Ocorrências de 'a': {}\n", .{count});

    // Substituir
    var buf: [256]u8 = undefined;
    const resultado = mem.replace(u8, texto, "incrível", "fantástica", &buf);
    std.debug.print("Substituído: {s} ({} substituições)\n", .{ buf[0..resultado], resultado });

    // Split
    var iter = mem.splitSequence(u8, texto, " ");
    while (iter.next()) |palavra| {
        std.debug.print("Palavra: {s}\n", .{palavra});
    }

    // Trim
    const com_espacos = "  Olá, mundo!  ";
    const trimmed = mem.trim(u8, com_espacos, " ");
    std.debug.print("Trimmed: '{s}'\n", .{trimmed});
}

zig-regex — Regex Nativa em Zig

A biblioteca zig-regex implementa um motor de expressões regulares em Zig puro:

const regex = @import("zig-regex");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Compilar padrão
    var re = try regex.Regex.compile(allocator, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
    defer re.deinit();

    // Testar match
    const texto = "Contato: maria@exemplo.com ou suporte@empresa.com.br";

    if (try re.match(texto)) |m| {
        std.debug.print("Email encontrado: {s}\n", .{m.slice});
    }

    // Encontrar todas as ocorrências
    var iter = re.iterator(texto);
    while (try iter.next()) |m| {
        std.debug.print("Match: {s}\n", .{m.slice});
    }
}

Padrões Suportados

// Caracteres literais
try regex.Regex.compile(allocator, "abc");

// Classes de caracteres
try regex.Regex.compile(allocator, "[a-z]");       // range
try regex.Regex.compile(allocator, "[^0-9]");      // negação
try regex.Regex.compile(allocator, "\\d");          // dígito
try regex.Regex.compile(allocator, "\\w");          // word char
try regex.Regex.compile(allocator, "\\s");          // whitespace

// Quantificadores
try regex.Regex.compile(allocator, "a*");           // zero ou mais
try regex.Regex.compile(allocator, "a+");           // um ou mais
try regex.Regex.compile(allocator, "a?");           // zero ou um
try regex.Regex.compile(allocator, "a{3}");         // exatamente 3
try regex.Regex.compile(allocator, "a{2,5}");       // 2 a 5

// Âncoras
try regex.Regex.compile(allocator, "^inicio");      // início da string
try regex.Regex.compile(allocator, "fim$");          // fim da string

// Grupos
try regex.Regex.compile(allocator, "(ab)+");         // grupo de captura
try regex.Regex.compile(allocator, "(?:ab)+");       // grupo sem captura

// Alternação
try regex.Regex.compile(allocator, "gato|cachorro");

PCRE2 via Interop C

Para regex completa com todas as funcionalidades PCRE2:

const c = @cImport({
    @cInclude("pcre2.h");
});

pub fn matchPcre(padrao: []const u8, texto: []const u8) !bool {
    var errorcode: c_int = undefined;
    var erroroffset: usize = undefined;

    const re = c.pcre2_compile_8(
        padrao.ptr,
        padrao.len,
        0,
        &errorcode,
        &erroroffset,
        null,
    ) orelse return error.CompilationFailed;
    defer c.pcre2_code_free_8(re);

    const match_data = c.pcre2_match_data_create_from_pattern_8(re, null)
        orelse return error.MatchDataFailed;
    defer c.pcre2_match_data_free_8(match_data);

    const rc = c.pcre2_match_8(
        re,
        texto.ptr,
        texto.len,
        0,
        0,
        match_data,
        null,
    );

    return rc >= 0;
}

Regex Comptime

Uma das possibilidades mais interessantes do Zig é compilar expressões regulares em tempo de compilação:

fn comptimeRegex(comptime padrao: []const u8) type {
    return struct {
        // O padrão é analisado e otimizado em comptime
        const automato = comptime parsePattern(padrao);

        pub fn match(texto: []const u8) bool {
            var estado: usize = 0;
            for (texto) |char| {
                estado = automato.transicao(estado, char);
                if (estado == automato.estado_final) return true;
            }
            return false;
        }
    };
}

// Uso
const EmailMatcher = comptimeRegex("[a-zA-Z0-9]+@[a-zA-Z]+\\.[a-z]{2,4}");

test "regex comptime" {
    try std.testing.expect(EmailMatcher.match("user@exemplo.com"));
    try std.testing.expect(!EmailMatcher.match("invalido"));
}

Parsers Combinadores

Para parsing mais complexo que regex, parsers combinadores são uma alternativa elegante:

const Parser = struct {
    pub fn literal(comptime expected: []const u8) fn ([]const u8) ?[]const u8 {
        return struct {
            fn parse(input: []const u8) ?[]const u8 {
                if (std.mem.startsWith(u8, input, expected)) {
                    return input[expected.len..];
                }
                return null;
            }
        }.parse;
    }

    pub fn digit(input: []const u8) ?struct { valor: u8, resto: []const u8 } {
        if (input.len > 0 and input[0] >= '0' and input[0] <= '9') {
            return .{ .valor = input[0] - '0', .resto = input[1..] };
        }
        return null;
    }

    pub fn manyDigits(input: []const u8) struct { valor: []const u8, resto: []const u8 } {
        var i: usize = 0;
        while (i < input.len and input[i] >= '0' and input[i] <= '9') : (i += 1) {}
        return .{ .valor = input[0..i], .resto = input[i..] };
    }
};

Casos de Uso Comuns

Validação de CPF

pub fn validarCpf(cpf: []const u8) bool {
    // Remover pontuação
    var digitos: [11]u8 = undefined;
    var pos: usize = 0;
    for (cpf) |c| {
        if (c >= '0' and c <= '9') {
            if (pos >= 11) return false;
            digitos[pos] = c - '0';
            pos += 1;
        }
    }
    if (pos != 11) return false;

    // Validar dígitos verificadores
    var soma: u32 = 0;
    for (0..9) |i| {
        soma += digitos[i] * @as(u32, @intCast(10 - i));
    }
    var resto = (soma * 10) % 11;
    if (resto == 10) resto = 0;
    if (resto != digitos[9]) return false;

    soma = 0;
    for (0..10) |i| {
        soma += digitos[i] * @as(u32, @intCast(11 - i));
    }
    resto = (soma * 10) % 11;
    if (resto == 10) resto = 0;
    return resto == digitos[10];
}

Boas Práticas

  1. Prefira std.mem para buscas simples: Mais rápido e sem dependências
  2. Use regex para padrões complexos: Quando busca literal não é suficiente
  3. Compile regex uma vez: Reutilize objetos compilados
  4. Considere comptime: Para padrões conhecidos, compile em tempo de compilação
  5. Parsers combinadores para gramáticas: Quando regex se torna ilegível

Próximos Passos

Explore as bibliotecas JSON para parsing estruturado, as bibliotecas CLI que usam pattern matching internamente, e os frameworks de teste para testar seus padrões. Consulte nossos tutoriais para projetos práticos.

Continue aprendendo Zig

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