Pular para o conteúdo

Stdlib · tier 1

regex

expressões regulares (multi-engine)

official_libraries.md · 166 linhas · 5 min de leitura

use regex

Três engines com garantias diferentes, escolhidos por construção via sub-namespace: RE2 (default, tempo linear, sem ReDoS), PCRE (backtracking, features completas, ReDoS-prone), HyperScan (multi-pattern + streaming, superfície própria). Um núcleo comum de operações (is_match/find/captures/replace/split) mais os extras por engine. Padrão literal compila em comptime (valida + embarca; inválido = erro de compilação); padrão de runtime compila em runtime. O engine é pesado e volátil, daí tier 1; o algoritmo RE2 é frozen (tier 0), empacotado numa lib que evolui.

O engine é parte da construção, não um parâmetro depois. regex.compile é o atalho do default (RE2); os outros vêm dos sub-namespaces. RE2 e PCRE devolvem o mesmo tipo Regex (núcleo comum); HyperScan funciona de outra forma e tem superfície própria.

regex.compile(pattern) → Regex via RE2 (o default)
regex.re2.compile(pattern) → Regex via RE2 (explícito)
regex.pcre.compile(pattern) → Regex via PCRE
regex.hyperscan.compile(pats) → Scanner via HyperScan (ver "HyperScan", abaixo)

Assinatura (idêntica em regex.compile/re2.compile/pcre.compile; muda só o engine que valida e casa):

fn compile(pattern: string) -> Result[Regex, RegexError] // RE2 (default)
fn compile_with(pattern: string, opts: Options) -> Result[Regex, RegexError]
alias RegexError = error{
Malformed, // sintaxe de padrão inválida
Unsupported, // feature ausente no engine escolhido (backref/lookaround no RE2)
TooLarge // padrão excede os limites do engine (RE2 tem teto de programa)
}

Padrão literal compila em comptime: valida (literal inválido vira erro de compilação, não Err de runtime) e embarca o programa compilado no binário (custo-zero de compilação em runtime). Por isso um literal pode ser .or_panic() sem risco: o comptime já provou que casa, ou falhou a build. Padrão de runtime compila em runtime e devolve Result. (Espelha a história de const/string, §16: o literal é comptime-na-fonte; o binding decide.)

const WORD := regex.compile("[a-z_][a-z0-9_]*").or_panic() // comptime: valida + embarca; nunca panica
user_re := regex.compile(padrao_do_usuario) catch |e| { return e } // runtime: Result tratado normalmente

Feature não-suportada pelo engine segue a mesma régua: padrão conhecido dá erro de compilação; padrão de runtime dá Err(Unsupported). (regex.compile("(a)\\1"), uma backreference, falha no RE2; regex.pcre.compile aceita.)

decl Regex { ... }
fn (r: Regex) is_match(text: string) -> bool
fn (r: Regex) find(text: string) -> Optional[Match] // 1º match
fn (r: Regex) find_all(text: string) -> Iterator[Match] // todos (lazy)
fn (r: Regex) captures(text: string) -> Optional[Captures] // 1º match com grupos
fn (r: Regex) captures_all(text: string) -> Iterator[Captures]
fn (r: Regex) replace(text: string, replacement: string) -> string // só o 1º
fn (r: Regex) replace_all(text: string, replacement: string) -> string // todos
fn (r: Regex) split(text: string) -> Iterator[string]
fn (r: Regex) pattern() -> string // o padrão fonte

No replacement, grupos são referenciados por $1 (numerado) e ${nome} (nomeado); $$ é um $ literal.

Cada Match carrega o trecho casado (view da entrada) e a faixa de bytes:

decl Match {
pub text: string // o trecho casado (slice/view da entrada)
pub start: usize // offset de byte inicial
pub end: usize // offset de byte final (exclusivo)
}
decl Captures { ... }
fn (c: Captures) get(i: usize) -> Optional[Match] // grupo numerado (0 = o match inteiro)
fn (c: Captures) get(name: string) -> Optional[Match] // grupo nomeado, dispatch por tipo (§13)
fn (c: Captures) len() -> usize // nº de grupos
fn (c: Captures) iter() -> Iterator[Optional[Match]] // grupos em ordem (none = não-participante)

get é um nome com dois tipos de argumento (§13): caps.get(1) (numerado) e caps.get("ano") (nomeado). Grupo opcional que não participou do match é none, daí o Optional.

re := regex.compile("(?P<ano>\\d{4})-(?P<mes>\\d{2})").or_panic()
match re.captures("2026-06") { // captures → Optional
none => { return }
caps => { ano := caps.get("ano").or_panic().text } // "2026"
}
decl Options {
pub unicode: bool // true: classes/`.` em codepoints + tabelas Unicode; false: ASCII/bytes
pub case_insensitive: bool
pub multi_line: bool // ^/$ casam nas quebras de linha
pub dot_all: bool // `.` casa '\n'
pub swap_greed: bool // inverte greedy/lazy
}

unicode é false por default (ASCII-rápido, coerente com unicode/strings serem opt-in/heavy): . e \w/\d operam em bytes. Ligando unicode, . casa um codepoint e as classes usam as tabelas Unicode (que aí pesam). Você escolhe o custo na construção, em vez de a linguagem escolher por você.

HyperScan: multi-pattern e streaming (superfície própria)

Seção intitulada “HyperScan: multi-pattern e streaming (superfície própria)”

HyperScan tem semântica distinta: casar N padrões simultaneamente sobre um fluxo, com callback por acerto (ferramenta de IDS/DPI, não regex geral). Daí Scanner, não Regex, e é o engine de streaming (a razão de existir dele):

decl Scanner { ... }
fn hyperscan.compile(patterns: []string) -> Result[Scanner, RegexError] // muitos padrões → um Scanner
fn hyperscan.compile_ids(patterns: []Pattern) -> Result[Scanner, RegexError] // com id por padrão
decl Pattern { pub expr: string; pub id: u32; pub opts: Options }
fn (s: Scanner) scan(text: string, on_match: fn(MatchEvent)) // sobre string
fn (s: Scanner) scan_stream(r: Readable, on_match: fn(MatchEvent)) -> error{Io} // sobre fluxo (streaming)
decl MatchEvent {
pub id: u32 // qual padrão casou (o id do Pattern; índice na lista quando sem id)
pub start: usize // offset no fluxo
pub end: usize
}

scan_stream sobre um Readable é o match sobre fluxo: HyperScan casa incrementalmente sem materializar a entrada. (RE2 faz streaming parcial, match/no-match incremental, sem captures plenos, anotado como direção futura no Regex; PCRE é string-only, não streama.)


  • Multi-engine, com o engine escolhido na construção (regex.re2/pcre/hyperscan; regex.compile é RE2). Os três têm garantias diferentes, e um Regex único fingindo que são iguais quebra (backref compila no PCRE, falha no RE2). Núcleo comum mais extras por engine, não um denominador que mente.
  • RE2 default, com garantia linear (sem ReDoS): padrão de entrada não-confiável é seguro. O custo honesto é ficar sem backreference/lookaround (Unsupported). É o diferencial de segurança, então é o default.
  • PCRE, com features completas mas ReDoS-prone: backref/lookaround/recursão, ao custo de blow-up exponencial. Só com padrão confiável. Mesma API; muda o que compila e a garantia de tempo.
  • HyperScan, superfície própria (multi-pattern + streaming, Scanner/callback), porque tem outra semântica (casar N sobre um fluxo), não regex geral. É onde mora o match sobre Readable.
  • Unicode configurável na construção, ASCII-rápido por default: Options.unicode liga codepoints + tabelas; o default é ASCII/bytes. As tabelas só pesam quando você opta (opt-in, como unicode).
  • Comptime se conhecido: literal compila em comptime (valida + embarca; inválido = erro de compilação), então .or_panic() num literal nunca panica; runtime compila em runtime (Result).
  • Captures por dispatch de tipo: get(i)/get(nome), um nome e dois tipos de argumento (§13). Cada Match carrega o trecho mais a faixa de bytes; grupo não-participante é none.
  • Sem allocator nas assinaturas (colorless); find_all/split/captures_all são Iterator lazy; replace aloca pelo @mm do contexto.