Modules

Regex

moduleSource
macOSLinuxWindowswasm

#import "Extensions/Regex";

Regular expressions with the syntax and guarantees of Rust's regex crate, full Unicode support, and patterns that can be compiled while your program compiles. A jaic extension, not official Jai: other Jai compilers have no such module, and it may change between jaic releases. See docs/extensions/regex.md.

ExampleOpen in playground ↗
Regex :: #import "Extensions/Regex";

// Compiled while the program compiles: a bad pattern is a compile error, and named
// groups become typed fields (an integer for a run of digits, an enum for a few words).
Date :: Regex.Static("(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})");
date, ok := Regex.parse(Date, "2026-10-11");  // date.year is a u16, date.month a u8

// Compiled at run time, for a pattern only known then.
user_pattern := "[a-z]+";
text := "one two";
re, compiled, error := Regex.compile(user_pattern);
for Regex.find_all(*re, text) print("% ", it.text);
Show moreShow less

Matching takes time linear in the text for every pattern: there are no backreferences and no look-around, and the engines are automata, never an exponential backtracker.

Procedures#

General#

(pattern: string, options := Options.{}) -> (Regex, ok: bool, Error)

Compiles pattern. The result says whether it was valid and, if not, the error says where it went wrong. Free the regex with free_regex.

is_one_pass2 overloads
(re: *Regex) -> bool

Whether captures for this pattern are found in a single scan (see docs/extensions/regex.md).

is_match2 overloads
(re: *Regex, text: string, start := 0) -> bool
find2 overloads
(re: *Regex, text: string, start := 0) -> (Match, bool)

The leftmost match at or after byte start.

find_all2 overloads
(re: *Regex, text: string) -> [..] Match

Every match, left to right, not overlapping. An empty match right where the previous match ended is skipped, and in Unicode mode empty matches never split a UTF-8 sequence.

group2 overloads
(caps: Captures, index: int) -> (string, bool)

Group index's text, and whether it took part in the match.

(caps: Captures, index: int) -> (start: int, end: int)

Group index's byte range, or -1, -1.

replace2 overloads
(re: *Regex, text: string, replacement: string) -> string

Replaces the first match. In replacement, $1 / ${1} is a group's text, $name / ${name} a named group's, and $$ a dollar sign; a group that did not match (or does not exist) gives "". $name takes the longest run of letters, digits and _, so write ${1}st rather than $1st.

replace_all2 overloads
(re: *Regex, text: string, replacement: string) -> string
replace_n2 overloads
(re: *Regex, text: string, replacement: string, limit: int) -> string

Replaces the first limit matches (all of them when limit < 0).

split2 overloads
(re: *Regex, text: string) -> [..] string

The pieces of text between matches.

(text: string) -> string

text with every regex metacharacter escaped, so it matches literally.

($R: Type, text: string) -> (R.Groups, bool)

The whole of text against the pattern, with its named groups parsed.

($R: Type, text: string, out: *$T) -> bool

Like parse, into your own struct: each field takes the named group of the same name, and is a string, integer, float, bool (true / false) or enum (a member's name, any case). A field with no group or a group with no field is a compile error. False if the text does not match or a group's text does not fit its field.

($R: Type, text: string, out: *$T, start := 0) -> bool

Like captures, into your own struct (see parse_into).

Syntax#

Program#

(parsed: *Parsed, reverse: bool) -> (Program, ok: bool, error: string)

Compiles root. reverse builds the program that reads the text backwards (no captures and no unanchored start: it is only run back from the end of a match).

(node: *Node) -> bool

Can only ever match the empty string at a position (a look or nothing), so it can be skipped over when looking for what a concatenation starts with.

NFA#

(look: Look, text: string, pos: int) -> bool

Whether look holds at pos. The whole text is context, so a search that starts mid-text still sees the byte before it.

Onepass#

(program: *Program, state_limit: int) -> OnePass

Builds the table for program (the forward one). ok is false in the result if the pattern is not one-pass or needs more than state_limit states.

DFA#

(d: *Dfa, program: *Program, reverse: bool, state_limit := DEFAULT_STATE_LIMIT)
(d: *Dfa) -> bool

Builds every state, so that no search ever needs the program. False if there are more than the limit.

(d: *Dfa) -> Dfa

A copy holding only the tables (for a DFA built completely).

(d: *Dfa, text: string, start: int, anchored: bool, earliest: bool) -> (end: int, gave_up: bool)

The end of the leftmost-first match that starts at or after start (and exactly at start if anchored), or -1. With earliest, stops at the first position where any match ends, which answers "is there a match" sooner. gave_up means the DFA grew past its limit and the caller has to use another matcher.

(d: *Dfa, text: string, bound: int, end: int) -> (start: int, gave_up: bool)

Run back from end (a reverse DFA): the leftmost position >= bound where a match ending at end can start, or -1.

(d: *Dfa, s: u32, text: string, at: int) -> u32

The step over non-ASCII byte text[at] in a DFA with a Unicode \b, through the column that knows whether a word character starts at at and whether one ends with it.

(d: *Dfa, index: int) -> *Skip

The skip for an unanchored start (computing it the first time, in a lazy DFA).

(skip: *Skip, text: string, i: int, fence: *int) -> int

The next position at or after i where a match could start, or -1.

(skip: *Skip, text: string, at: int) -> bool

Whether the bytes from at can begin a match, as far as the skip's checks tell.

(program: *Program) -> (sets: [..][256] bool, exact: bool)

For each of the first few positions of a match, the bytes it can have there, up to the shortest match's length (looks are taken to hold, which only adds bytes).

(program: *Program) -> (required: [256] bool, before: [256] bool, ok: bool)

A rare set of bytes that every match contains, and the bytes a match can have before the first of them. Tries each ASCII byte together with its other case.

(program: *Program, required: [256] bool) -> ([256] bool, ok: bool)

The bytes a match can consume before its first byte from required; not ok if a match can do without one.

(members: [256] bool) -> float64

A rough share of typical text (mostly English, some UTF-8) that falls in the set.

(engine: *Engine, text: string, start: int, earliest := false) -> (start: int, end: int)

The leftmost-first match at or after start: its start and end, or end -1. earliest only promises to say whether there is one (the end may be early and the start is not computed).

(program: *Program) -> int

The length every match has, or -1 if they differ (or the program is too big to tell).

(engine: *Engine, text: string, s: int, e: int, slots: [] int)

Where each group of the match text[s .. e] is.

(the_engine: *Engine, text: string, body: Code)

Runs body for each match with s and e set, following find_all's rules for empty matches.

Generate#

(pattern: string, full: bool) -> Engine

The engine for a constant pattern, built at compile time. A bad pattern is a compile error.

(pattern: string) -> string

Static's check that its pattern parses; returns no code.

(R: Type) -> (bool, string)

The #modify of every procedure that takes a Static type.

(pattern: string) -> ([..] Field_Plan, ok: bool)

One plan per named group, in group order. ok is false if the pattern does not parse (the error is reported where the engine is built).

(node: *Node) -> (string, bool)

The letter a case-insensitive class stands for, written in lower case: (?i)y matches Y and y. Other characters in the class must be outside ASCII (like the Kelvin sign for k).

(word: string) -> string

An identifier in upper snake case, the way enum members are written.

(node: *Node) -> (string, bool)

The text a node always matches exactly, if it is a plain string.

(node: *Node) -> (signed: bool, digits: int, is_float: bool, ok: bool)

Whether a node only matches numbers: an optional sign, digits, and for a float a . and more digits. digits is the most integer digits it can match, -1 for no limit.

(node: *Node) -> (ok: bool, lo: int, hi: int)

Matches only decimal digits; how many (hi -1: no limit).

(pattern: string) -> string

The members of Groups(pattern).

(pattern: string) -> string

Statements that fill out (a *Groups(pattern)) from text and slots; return false when a number does not fit its field.

(pattern: string, T: Type) -> string

Statements that fill out (a *T the caller declared) from text and slots. Every field must have a named group and every named group a field; anything else is a compile error.

(piece: string, $T: Type) -> (T, bool)

Decimal digits (any script's) with an optional sign, into an integer of type T. False when it does not fit.

(piece: string) -> (float64, bool)

A number like 12.5 or -3 (digits from any script) as a float64.

(text: string, index: int) -> (int, int)

The value of the decimal digit at index and its size in bytes; -1 if it is not one.

Types#

Optionsstruct
:: struct

Pattern flags, also settable inside the pattern: (?i) (?m) (?s) (?U) (?x) (?R) (?-u).

Regexstruct
:: struct

A compiled pattern. Copies share the compiled program and the search cache, which is not safe to use from two threads at once: give each thread its own compile.

Capturesstruct
:: struct

Where each group matched. Group 0 is the whole match; a group that did not take part has start -1.

Staticstruct
:: struct

A regex compiled while your program compiles: a type, made from a constant pattern.

Regex :: #import "Extensions/Regex";
Date :: Regex.Static("(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})");
date, ok := Regex.parse(Date, "2026-10-11");  // a Date.Groups: date.year is a u16
↗
Nodestruct
:: struct
Inststruct
:: struct
Edgestruct
:: struct
Dfastruct
:: struct
Skipstruct
:: struct

An unanchored start state usually goes back to itself on most bytes: the search is waiting for the first byte of a match. A vector search jumps to the next place a match could start. Without looks, any position will do as a restart (the start state does not depend on the byte before), so the search can use bytes from further into the match, at a fixed distance from its start: two of them, if no single one is rare. When every byte near the start is common, a rare byte that every match must contain will do: find it, then walk back over the bytes a match can have before it to where the match could start. With looks, it can only jump over bytes that keep the DFA in its start state.

:: enum

What a named group's field holds.

Constants#

:: 0xFFFF_FFFF

as in Rust; big counts still fail as too large a program

:: 0xFFFF_FFFE

A non-ASCII byte in a DFA with a Unicode \b: look at the characters around it (see unicode_word).

:: 0

What a byte looks like to a look: the edge of the text, \n, \r, a word byte, anything else.

:: 0.004

A byte rarer than this is searched for alone.

:: 0.25

A search whose bytes make up more than this share of typical text stops too often to pay.

:: 0.03

A required byte rarer than this pays for walking back over the bytes before it.

:: #run -> [256] float64 { share: [256] float64; for b: 0..255 { NON_ASCII_SHARE :: 0.004; UPPER_AND_DIGIT_SHARE :: 0.003; PUNCTUATION_SHARE :: 0.01; SPACE_SHARE : ...
:: 256

Bigger than this and the DFA is built lazily at run time instead of stored in the program.

:: string.["if", "else", "then", "case", "for", "while", "break", "continue", "return", "defer", "using", "cast", "xx", "ifx", "struct", "union", "enum", "enum_fla ...