slang

regex

Package regex.

Markdown version · llms.txt · api.json

Regular expressions on str or bytes, matched by slang's own Thompson NFA — no external library, so a program that matches text stays as dependency-free as a plain TCP one. compile returns an opaque rawptr handle (freed with regex.free, like net.tls_* and sql), and a bad pattern comes back as a descriptive result[rawptr, str].

Matching is linear time, always. There is no backtracking, so the classic catastrophic pattern (a+)+$ — which makes a backtracking engine take exponential time on a hostile input — runs in the same microseconds here as any other pattern. That is the point of choosing this engine for a server language: patterns and subjects both arrive from the network. The price is the RE2/Go one, and it is not negotiable: no backreferences and no lookaround. Both require backtracking; (?=...) and friends are a compile error, not a silent mis-parse.

FunctionSignature
regex.compile(pat)result[rawptr, str]
regex.free(re)
regex.groups(re)int — number of capture groups
regex.is_match(re, s)bools is a str
regex.is_match_bytes(re, b)boolb is bytes
regex.find(re, s) / find_bytes(re, b)[int]
regex.find_at(re, s, from) / find_bytes_at(re, b, from)[int]

find returns byte offsets as [start, end, g1start, g1end, ...], or an empty list when there is no match — so the result is GC-owned and there is no match handle to leak. find_at starts at an offset, which is how you walk every match.

import "regex";

let cr = regex.compile("(\\d{4})-(\\d{2})-(\\d{2})");
guard let re = cr else let e = err_of(cr) {
    log.error("bad pattern: " + e);   // e.g. "missing ) at offset 9"
    exit(1);
}

if regex.is_match(re, "due 2026-09-09") {
    let m = regex.find(re, "due 2026-09-09");
    println(to_str(m[0]) + ".." + to_str(m[1]));   // whole match: 4..14
    println(to_str(m[2]) + ".." + to_str(m[3]));   // year:        4..8
}
regex.free(re);

Supported: literals, ., classes [a-z] [^...] [[:digit:]], escapes \d \D \w \W \s \S \b \B \A \z \xHH, quantifiers * + ? {n} {n,} {n,m} and their lazy ? forms, groups (...) and (?:...), alternation |, anchors ^ $. Matching is leftmost-first (Perl-style priority), and . does not match \n.

Subjects are matched with an explicit length, so a bytes containing NUL matches correctly rather than stopping at the NUL — and \D matches a NUL byte like any other non-digit.

Bounds, so a hostile pattern can't exhaust memory or stack: 4096 compiled instructions, 100 nesting levels, 32 capture groups, and {n,m} counts up to 1000. Each is a descriptive compile error, never a crash.

A compiled regex is safe to share across tasks, and is meant to be: it carries a small pool of reusable match buffers, so concurrent matchers allocate nothing per match. Compiling is the expensive part (it is also the only part that grows the task stack) — compile once, match many times, ideally not once per request.

Where this lands on speed. Measured single-threaded on ^(GET|POST|PUT) (/[a-z0-9/_-]*) HTTP/1\.([01])$ against a 26-byte subject, 200k iterations:

enginematches/secon (a+)+$ vs a hostile input
slang regex~721k2µs, correct answer
POSIX regexec~372kfast here, but no limits
PCRE2 (interpreted)~2.4M0.2s, then gives up (MATCHLIMIT)
PCRE2 (JIT)~8.6Msame — JIT does not save it

So: ~1.9x faster than libc's POSIX engine, and several times slower than PCRE2 on benign input — PCRE2's interpreter and especially its JIT are very good, and this is an honest gap. The trade is deliberate: on adversarial input the ordering inverts completely, because linear time is a guarantee here and a hope there. is_match is markedly cheaper than find (it binds no capture slots at all), so prefer it when you only need a yes/no. Matching scales with tasks — ~3.2M/sec across 16.

Because matching never grows the task stack, regex is cheap to use per connection: 600 concurrently-live tasks each matching and then parking peak at 3.9MB RSS — about 17x lighter than the same shape holding sql connections (65.9MB), which does grow every task's stack.

API#

regex.compile(str) -> result[rawptr,str]#
regex.free(rawptr)#
regex.groups(rawptr) -> int#
regex.is_match(rawptr, str) -> bool#
regex.is_match_bytes(rawptr, bytes) -> bool#
regex.find(rawptr, str) -> [int]#
regex.find_at(rawptr, str, int) -> [int]#
regex.find_bytes(rawptr, bytes) -> [int]#
regex.find_bytes_at(rawptr, bytes, int) -> [int]#