blob: 693b6bddb6f42c69fa5a0970405314b2d1c686b1 [file]
// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// DFA regex executor for tokenizer pre-tokenization.
//
// This is the minimal runtime component of the regex engine. It executes
// pre-compiled DFAs to find pattern matches in UTF-8 text. The executor is
// designed for streaming operation with O(1) state and zero heap allocation.
//
// Architecture:
// The regex system is split into two libraries:
// 1. Executor (regex/exec.h) - Minimal runtime for executing pre-compiled DFA
// 2. Compiler (regex/compile.h) - Pattern string to DFA compilation
//
// This separation allows embedding minimal runtime code when DFAs are
// pre-compiled at build time, while still supporting on-demand compilation
// from JSON tokenizer configs at runtime when needed.
//
// Supported Features (covers 95%+ of HuggingFace tokenizers):
// - Literals: a, ab, abc
// - Any character: .
// - Character classes: [abc], [a-z], [^abc]
// - Escapes: \r, \n, \t, \f, \v, \a, \e, \0, \\, \[, etc.
// - Shorthand classes: \d, \D, \w, \W, \s, \S
// - Unicode categories: \p{L}, \p{N}, \p{P}, \p{M}, \p{S}, \p{Z}, \p{C}
// - Quantifiers: *, +, ?, {n}, {n,}, {n,m}
// - Alternation: a|b|c
// - Grouping: (...), (?:...)
// - Negative lookahead: (?!x), (?!\S) (end of branch, single char/shorthand)
// - Anchors: ^, $
//
// Not supported (rare, would break DFA properties):
// - Backreferences: \1, \2 (requires backtracking)
// - Positive lookahead: (?=...) (complex state machine)
// - Lookbehind: (?<=...), (?<!...) (requires backward scan)
// - Named groups: (?P<name>...) (capture not needed)
// - Atomic groups: (?>...) (backtracking construct)
// - Recursion: (?R) (requires stack)
// - Negated Unicode: \P{...} (use [^\p{...}] instead)
//
// Shorthand Class Semantics:
// - \d = [0-9] (ASCII digits only, not Unicode Nd category)
// - \D = [^0-9] (anything except ASCII digits)
// - \w = [a-zA-Z0-9_] (ASCII word chars only, not Unicode letters/digits)
// - \W = [^a-zA-Z0-9_] (anything except ASCII word chars)
// - \s = ASCII whitespace + Unicode White_Space property (via
// PSEUDO_WHITESPACE)
// - \S = non-whitespace (complement of \s)
//
// Note: \d and \w are ASCII-only for compatibility with Python's re module
// default behavior and most tokenizer patterns. Use \p{N} and [\p{L}\p{N}_]
// for Unicode-aware digit/word matching.
//
// Matching Semantics:
// - Leftmost-longest: Matches start as early as possible, longest wins
// - Search mode: The executor finds matches anywhere in input (not anchored)
// - After failed partial match, executor rescans from match_start + 1
// - Example: Pattern "ab" on "aab" finds match at position 1
//
// Streaming:
// Streaming mode uses an internal rewind buffer to handle cross-chunk
// backtracking. When a partial match starts in chunk A and fails in chunk B,
// the executor can backtrack properly using buffered bytes from chunk A.
//
// This covers all practical tokenizer patterns (whitespace runs, word
// boundaries, punctuation). If a pattern requires backtracking more than the
// buffer size, exec_feed() returns IREE_STATUS_OUT_OF_RANGE.
//
// Why DFA:
// - O(n) guaranteed time complexity (patterns run millions of times)
// - Streamable execution with checkpoint/resume support
// - Pre-compilable to compact binary format
// - No backtracking means predictable, bounded memory usage
//
// API Organization:
//
// Step function (inline, lowest level):
// iree_tokenizer_regex_dfa_step() - single transition lookup
//
// Streaming executor (core API):
// iree_tokenizer_regex_exec_initialize() - initialize state
// iree_tokenizer_regex_exec_feed() - process chunk, emit matches
// iree_tokenizer_regex_exec_finalize() - flush pending match
//
// Convenience wrappers:
// iree_tokenizer_regex_exec() - full-text execution
//
// Thread Safety:
// - DFA loading: Thread-safe (read-only on input).
// - DFA execution: Thread-safe for same DFA with different state/text.
// Multiple threads can execute the same DFA concurrently as long as each
// has its own exec_state_t.
#ifndef IREE_TOKENIZER_UTIL_REGEX_EXEC_H_
#define IREE_TOKENIZER_UTIL_REGEX_EXEC_H_
#include "iree/base/api.h"
#ifdef __cplusplus
extern "C" {
#endif
//===----------------------------------------------------------------------===//
// Binary Format Constants
//===----------------------------------------------------------------------===//
// Magic number for DFA binary format: "ITDF" (IREE Tokenizer DFA).
#define IREE_TOKENIZER_UTIL_REGEX_DFA_MAGIC \
(((uint32_t)'I') | ((uint32_t)'T' << 8) | ((uint32_t)'D' << 16) | \
((uint32_t)'F' << 24))
// Current DFA binary format version.
// Increment when making breaking changes to the format.
//
// Layout: header + transitions + bitmap + lookahead + anchors
// - Dense 256-entry transition table per state (512 bytes/state)
// - Maximum 65534 states (65535 reserved for NO_TRANSITION sentinel)
//
// ALIGNMENT: DFA data must be 8-byte aligned for zero-copy loading.
// The accepting_bitmap uses uint64_t which requires 8-byte alignment on
// strict architectures (ARM, SPARC). Unaligned data will be rejected by
// iree_tokenizer_regex_dfa_load() with INVALID_ARGUMENT status.
#define IREE_TOKENIZER_UTIL_REGEX_DFA_VERSION 1
// Maximum supported DFA version (for forward compatibility).
#define IREE_TOKENIZER_UTIL_REGEX_DFA_MAX_VERSION 1
// Sentinel value for "no transition" in the transition table.
#define IREE_TOKENIZER_UTIL_REGEX_NO_TRANSITION 0xFFFFu
// Size of the rewind buffer for streaming backtracking.
// This determines how many bytes can be buffered for cross-chunk backtracking.
// Patterns requiring more backtracking than this will return OUT_OF_RANGE.
#define IREE_TOKENIZER_UTIL_REGEX_REWIND_BUFFER_SIZE 256
//===----------------------------------------------------------------------===//
// Unicode Pseudo-Bytes
//===----------------------------------------------------------------------===//
// The DFA operates on bytes (0-255), but we need to match Unicode categories
// (\p{L}, \p{N}, etc.) which span multiple codepoints. We use "pseudo-bytes"
// in the range 0x80-0x8F (invalid as UTF-8 lead bytes) to represent categories.
//
// At runtime, the executor decodes UTF-8 codepoints, classifies them via
// iree_unicode_category(), and looks up transitions using the pseudo-byte.
// ASCII bytes (0x00-0x7F) are used directly.
//
// Example: For pattern \p{L}+, the DFA has transitions on PSEUDO_LETTER.
// When executing on "Hello", 'H' (0x48) uses direct lookup, but on "Héllo",
// the 'é' (U+00E9) is classified as Letter and uses PSEUDO_LETTER transition.
typedef enum iree_tokenizer_regex_pseudo_byte_e {
// Unicode categories (0x80-0x87).
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_LETTER = 0x80, // \p{L} - Letters
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_NUMBER = 0x81, // \p{N} - Numbers
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_PUNCT = 0x82, // \p{P} - Punctuation
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_MARK = 0x83, // \p{M} - Marks
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_SYMBOL = 0x84, // \p{S} - Symbols
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_SEPARATOR = 0x85, // \p{Z} - Separators
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_OTHER = 0x86, // \p{C} - Other
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_WHITESPACE =
0x87, // \s (non-ASCII whitespace)
IREE_TOKENIZER_UTIL_REGEX_PSEUDO_COUNT = 8,
} iree_tokenizer_regex_pseudo_byte_t;
//===----------------------------------------------------------------------===//
// Exact Codepoint Ranges
//===----------------------------------------------------------------------===//
// Maximum number of exact codepoint ranges in a character class.
// This limit keeps the match_class structure cache-friendly (2 cache lines)
// while supporting the Unicode ranges found in HuggingFace tokenizers
// (e.g., DeepSeek's [一-龥] or BLOOM's multi-script punctuation).
// Exceeding this limit produces a compile-time error with guidance to
// use \p{L} for broad matching instead of many literal ranges.
#define IREE_TOKENIZER_UTIL_REGEX_MAX_CHAR_CLASS_RANGES 10
// An exact Unicode codepoint range [start, end] (inclusive).
// Used for precise matching of literal Unicode ranges in character classes
// like [一-龥] (U+4E00-U+9FA5). Ranges are stored sorted by start codepoint
// to enable early-exit optimization during matching.
typedef struct iree_tokenizer_regex_codepoint_range_t {
// First codepoint in range (inclusive).
uint32_t start;
// Last codepoint in range (inclusive).
uint32_t end;
} iree_tokenizer_regex_codepoint_range_t;
// A DFA range transition maps a codepoint range to a target state.
// When the executor sees a codepoint in [start, end], it transitions to
// target_state. Range transitions are checked BEFORE pseudo-byte fallback,
// enabling exact matching that would otherwise over-approximate via categories.
//
// Binary format for range transitions section (when HAS_RANGES flag is set):
// uint16_t num_ranges // Total number of range transitions
// uint16_t reserved // Padding for 4-byte alignment
// iree_tokenizer_regex_dfa_range_transition_t[num_ranges] // Flat array
//
// The flat array design enables zero-copy loading without per-DFA allocation.
// At runtime, we linear-search the array for ranges matching the current state.
// This is O(n) in the number of ranges, but tokenizers typically have 0-4
// ranges total, so this is negligible compared to UTF-8 decoding overhead.
typedef struct iree_tokenizer_regex_dfa_range_transition_t {
// DFA state this range applies to.
uint16_t from_state;
// DFA state to transition to on match.
uint16_t target_state;
uint32_t start; // Range start (inclusive).
uint32_t end; // Range end (inclusive).
} iree_tokenizer_regex_dfa_range_transition_t;
static_assert(sizeof(iree_tokenizer_regex_dfa_range_transition_t) == 12,
"range transition must be 12 bytes");
//===----------------------------------------------------------------------===//
// DFA Header
//===----------------------------------------------------------------------===//
// DFA header flags.
typedef enum iree_tokenizer_regex_dfa_flag_bits_e {
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_NONE = 0,
// DFA has lookahead data (lookahead table follows accepting bitmap).
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_LOOKAHEAD = 1 << 0,
// DFA uses Unicode pseudo-bytes for category matching (\p{L}, \s, etc.).
// The executor always performs UTF-8 decoding; this flag indicates the
// DFA contains category-based transitions rather than ASCII-only patterns.
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_UNICODE = 1 << 1,
// DFA is case-insensitive (ASCII letters only).
// This flag is informational for tooling and debugging. The compiler
// is responsible for emitting transitions for both cases (e.g., 'a'
// and 'A' transitions for case-insensitive [a-z]). The executor does
// not perform runtime case folding.
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_CASE_INSENSITIVE = 1 << 2,
// DFA has anchor bitmaps (start_anchor and end_anchor bitmaps follow).
// Start anchor: states that require match to start at position 0.
// End anchor: accepting states that require match to end at end of input.
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_ANCHORS = 1 << 3,
// DFA has branch tracking bitmasks for PCRE-compatible alternation.
// alive_branches: which alternation branches can reach each state.
// accepting_branches: which branches accept at each state.
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_BRANCHES = 1 << 4,
// DFA has exact codepoint range transitions.
// Ranges are stored per-state and checked before pseudo-byte fallback.
// This enables precise matching of Unicode ranges like [一-龥].
IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_RANGES = 1 << 5,
} iree_tokenizer_regex_dfa_flag_bits_t;
typedef uint16_t iree_tokenizer_regex_dfa_flags_t;
// Binary DFA header. All fields are little-endian.
// The header is followed by:
// 1. Transition table: uint16_t[num_states][256]
// 2. Accepting bitmap: uint64_t[ceil(num_states/64)]
// 3. Lookahead table (if HAS_LOOKAHEAD):
// iree_tokenizer_regex_lookahead_t[num_states]
// 4. Start anchor bitmap (if HAS_ANCHORS): uint64_t[ceil(num_states/64)]
// States that require matching to start at position 0.
// 5. End anchor bitmap (if HAS_ANCHORS): uint64_t[ceil(num_states/64)]
// Accepting states that require matching to end at end of input.
// 6. Alive branches (if HAS_BRANCHES): uint64_t[num_states]
// For each state, which alternation branches can reach it.
// 7. Accepting branches (if HAS_BRANCHES): uint64_t[num_states]
// For each state, which branches accept at this state.
// 8. Range transitions (if HAS_RANGES):
// uint16_t num_ranges // Total range count
// uint16_t reserved // Padding for alignment
// iree_tokenizer_regex_dfa_range_transition_t[num_ranges] // Flat
// array
typedef struct iree_tokenizer_regex_dfa_header_t {
uint32_t magic; // = DFA_MAGIC.
uint16_t version; // Format version.
uint16_t flags; // iree_tokenizer_regex_dfa_flags_t.
uint16_t num_states;
// Number of accepting states (for validation).
uint16_t num_accepting;
// Start state for position 0 (includes ^ paths).
uint16_t start_state;
// Start state for position > 0 (no ^).
uint16_t unanchored_start_state;
} iree_tokenizer_regex_dfa_header_t;
static_assert(sizeof(iree_tokenizer_regex_dfa_header_t) == 16,
"DFA header must be 16 bytes");
//===----------------------------------------------------------------------===//
// Lookahead Support
//===----------------------------------------------------------------------===//
// Lookahead is used to implement negative lookahead assertions like (?!\S).
// Instead of compiling the lookahead pattern to a separate DFA (complex), we
// support only single-character or character-class lookaheads, which covers
// all known HuggingFace tokenizer patterns.
//
// Research finding (via analysis of tiktoken, GPT-2, SentencePiece, custom):
// - tiktoken/GPT-4, GPT-2, StarCoder: Only use \s+(?!\S)
// - SentencePiece models: Don't use regex lookahead at all
// - Custom tokenizers: May use (?!\]), (?![a-zA-Z]) - single char/class
// - No evidence of complex lookaheads like (?!foo|bar+) in production
enum iree_tokenizer_regex_lookahead_type_e {
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NONE = 0,
// Negative lookahead for single character: (?!x)
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_CHAR = 1,
// Negative lookahead for shorthand class: (?!\S), (?!\s), (?!\d), etc.
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_SHORTHAND = 2,
// Negative lookahead for character class: (?![abc]), (?![a-z])
// The class is stored as a 256-bit bitmap in lookahead_data.
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_CLASS = 3,
// Lookahead types with fallback (for alternation patterns like
// \s+(?!\S)|\s+). When alternation has both lookahead and non-lookahead
// branches, the DFA state is accepting for BOTH. The executor must track two
// positions:
// 1. Last position where lookahead passed (preferred)
// 2. Last position where fallback would accept (used if lookahead fails)
// This gives PCRE-like semantics where the lookahead branch has priority.
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_CHAR_WITH_FALLBACK = 4,
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_SHORTHAND_WITH_FALLBACK = 5,
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_NEG_CLASS_WITH_FALLBACK = 6,
};
typedef uint8_t iree_tokenizer_regex_lookahead_type_t;
// Flags for PCRE-compatible match selection in lookahead table.
// See nfa.h accept struct comment for full documentation on limitations.
typedef enum iree_tokenizer_regex_lookahead_flag_bits_e {
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_FLAG_NONE = 0,
// True if alternation has a non-lookahead branch BEFORE the first lookahead.
// When set, prefer longer match (independent branch extends greedily).
// When not set, prefer lookahead-passed position (fallback branches).
IREE_TOKENIZER_UTIL_REGEX_LOOKAHEAD_FLAG_HAS_EARLY_NO_LOOKAHEAD = 1u << 0,
} iree_tokenizer_regex_lookahead_flag_bits_t;
typedef uint8_t iree_tokenizer_regex_lookahead_flags_t;
// Lookahead data for a single state.
// For LOOKAHEAD_NEG_CHAR: data is the character to NOT match.
// For LOOKAHEAD_NEG_SHORTHAND: data encodes the shorthand type.
// For LOOKAHEAD_NEG_CLASS: data is offset into extended lookahead bitmap table.
typedef struct iree_tokenizer_regex_lookahead_t {
iree_tokenizer_regex_lookahead_type_t type;
uint8_t data; // Type-specific data
iree_tokenizer_regex_lookahead_flags_t flags; // PCRE-compatibility flags
uint8_t reserved; // Padding for alignment
} iree_tokenizer_regex_lookahead_t;
static_assert(sizeof(iree_tokenizer_regex_lookahead_t) == 4,
"lookahead entry must be 4 bytes");
// Shorthand class identifiers for LOOKAHEAD_NEG_SHORTHAND.
typedef enum iree_tokenizer_regex_shorthand_e {
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_s = 0, // \s - whitespace
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_S = 1, // \S - non-whitespace
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_d = 2, // \d - digit
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_D = 3, // \D - non-digit
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_w = 4, // \w - word character
IREE_TOKENIZER_UTIL_REGEX_SHORTHAND_W = 5, // \W - non-word character
} iree_tokenizer_regex_shorthand_t;
//===----------------------------------------------------------------------===//
// Match Result
//===----------------------------------------------------------------------===//
// A single match result from DFA execution.
typedef struct iree_tokenizer_regex_match_t {
// Byte offset of match start (inclusive).
iree_host_size_t start;
// Byte offset of match end (exclusive).
iree_host_size_t end;
} iree_tokenizer_regex_match_t;
// Callback invoked with match results during DFA execution.
// |match| contains the byte offsets of the match within the input text.
// Return non-OK status to abort execution.
typedef iree_status_t (*iree_tokenizer_regex_match_callback_fn_t)(
void* user_data, iree_tokenizer_regex_match_t match);
//===----------------------------------------------------------------------===//
// DFA Handle
//===----------------------------------------------------------------------===//
// Opaque handle to a loaded DFA.
// This is a lightweight view into the binary DFA data (zero-copy when mmap'd).
typedef struct iree_tokenizer_regex_dfa_t {
const iree_tokenizer_regex_dfa_header_t* header;
// [num_states][256] transition table.
const uint16_t* transitions;
// Bit i = state i is accepting.
const uint64_t* accepting_bitmap;
const iree_tokenizer_regex_lookahead_t* lookahead; // NULL if no lookahead.
const uint64_t* start_anchor_bitmap; // NULL if no anchors.
const uint64_t* end_anchor_bitmap; // NULL if no anchors.
const uint64_t* alive_branches; // NULL if no branches.
const uint64_t* accepting_branches; // NULL if no branches.
// Range transitions: flat array for zero-copy loading. NULL if no ranges.
// At runtime, linear-search for ranges matching current state.
const iree_tokenizer_regex_dfa_range_transition_t* ranges;
// Number of range transitions (0 if no ranges).
uint16_t num_ranges;
} iree_tokenizer_regex_dfa_t;
//===----------------------------------------------------------------------===//
// Streaming Execution State
//===----------------------------------------------------------------------===//
// State for streaming DFA execution.
//
// This allows feeding text in chunks while maintaining match state across
// calls. The state is O(1) size and can be stack-allocated.
//
// UTF-8 Requirement:
// Callers must provide complete UTF-8 codepoints. Multi-byte sequences
// must not be split across chunks. The tokenizer entry point enforces this
// via its own UTF-8 carryover handling before invoking the regex executor.
//
// Usage:
// iree_tokenizer_regex_exec_state_t state;
// iree_tokenizer_regex_exec_initialize(&state, &dfa);
// while (more_chunks) {
// iree_tokenizer_regex_exec_feed(&dfa, &state, chunk, offset, stride, cb,
// ud); offset += chunk.data_length;
// }
// iree_tokenizer_regex_exec_finalize(&dfa, &state, total_length, cb, ud);
//
// Cross-Chunk Backtracking:
// The executor implements leftmost-longest matching, which requires trying
// alternative match starts when the current path fails. When a partial match
// begins in chunk A and fails in chunk B, the executor uses an internal
// rewind buffer (IREE_TOKENIZER_UTIL_REGEX_REWIND_BUFFER_SIZE bytes).
//
// This handles all practical tokenizer patterns (whitespace, word boundaries,
// punctuation). If a pattern requires backtracking more than the buffer size,
// exec_feed() returns IREE_STATUS_OUT_OF_RANGE.
typedef struct iree_tokenizer_regex_exec_state_t {
// Start position of current potential match.
iree_host_size_t match_start;
// End position where lookahead passed (or unconditional accept if no
// lookahead).
iree_host_size_t last_accept;
uint16_t dfa_state; // Current DFA state.
// Whether we're tracking a potential match.
bool in_match;
// Whether we've seen an accepting state.
bool has_accept;
// Pending lookahead state for chunk boundary handling.
// When we reach an accepting state with lookahead at chunk end, we can't
// evaluate the lookahead until we see the next character. We store the
// pending state here and resolve it on the next chunk or at finalize.
// Waiting for next char for lookahead.
bool pending_lookahead;
// DFA state awaiting lookahead check.
uint16_t pending_lookahead_state;
// Match end position if accepted.
iree_host_size_t pending_match_end;
// Fallback acceptance for mixed lookahead/non-lookahead alternation.
// For patterns like \s+(?!\S)|\s+, the DFA state accepts for BOTH branches.
// We track positions separately:
// - last_accept/has_accept: Position where lookahead PASSED
// - last_accept_fallback/has_accept_fallback: Position that would accept
// without lookahead (fallback)
// At match boundary, we prefer lookahead-passed position. Only if no
// lookahead-passed position exists do we use the fallback.
// Have a fallback accept position.
bool has_accept_fallback;
iree_host_size_t last_accept_fallback; // Fallback position.
// Branch-aware best candidate tracking (for PCRE-compatible alternation).
// Only used when DFA has HAS_BRANCHES flag set.
// Tracks the highest-priority branch that has accepted so far.
// Lowest accepting branch (0=highest priority, 64=none).
uint8_t best_branch_idx;
// End position of best candidate.
iree_host_size_t best_match_end;
// Rewind buffer for cross-chunk backtracking.
//
// When a partial match starts in chunk A and fails in chunk B, proper
// leftmost-longest semantics require trying match_start + 1. If match_start
// is in a previous chunk, we need those bytes to backtrack correctly.
//
// At the end of each chunk, if we're in a partial match, we copy bytes from
// match_start to the end of the chunk into this buffer. On backtrack failure,
// we can then re-process from the buffer instead of silently skipping
// matches.
//
// The buffer size covers all practical tokenizer patterns (whitespace runs,
// word boundaries, punctuation). If a pattern requires more, exec_feed()
// returns IREE_STATUS_OUT_OF_RANGE.
uint8_t rewind_buffer[IREE_TOKENIZER_UTIL_REGEX_REWIND_BUFFER_SIZE];
iree_host_size_t rewind_buffer_length; // Valid bytes in buffer.
iree_host_size_t rewind_buffer_start; // Absolute offset of buffer[0].
} iree_tokenizer_regex_exec_state_t;
//===----------------------------------------------------------------------===//
// Stride (DFA execution acceleration)
//===----------------------------------------------------------------------===//
// Precomputed stride data, derived from the DFA transition table at load time.
// Enables the regex executor to take larger strides through ASCII input by
// skipping per-byte bookkeeping for boring states and scanning entire runs
// for self-loop states.
//
// A state is "boring" when it is non-accepting, has no lookahead, no anchors,
// and all branches are still alive (no branch-commit can trigger). For these
// states, the DFA transition lookup alone determines the next state — no
// bookkeeping is needed beyond updating dfa_state and position.
//
// Single slab allocation using IREE_STRUCT_LAYOUT:
// [iree_tokenizer_regex_stride_t header]
// [boring_bitmap: ceil(num_states/8) bytes]
// [self_loop_category: num_states bytes]
typedef struct iree_tokenizer_regex_stride_t {
// Byte equivalence classes for ASCII (0x00-0x7F only).
// Bytes with identical transition columns across ALL states get the same
// category. Max 128 categories (one per ASCII byte in worst case, but
// typically 8-15 for tokenizer patterns).
uint8_t category_table[128];
uint8_t category_count; // Number of distinct categories.
uint16_t num_states;
// Offsets into trailing slab (from start of this struct).
iree_host_size_t boring_bitmap_offset; // -> uint8_t[ceil(num_states/8)]
iree_host_size_t self_loop_category_offset; // -> uint8_t[num_states]
} iree_tokenizer_regex_stride_t;
// Returns the boring bitmap from a stride struct.
static inline const uint8_t* iree_tokenizer_regex_stride_boring_bitmap(
const iree_tokenizer_regex_stride_t* stride) {
return (const uint8_t*)((uintptr_t)stride + stride->boring_bitmap_offset);
}
// Returns true if the given state is "boring" (safe for stride fast path).
static inline bool iree_tokenizer_regex_stride_is_boring(
const iree_tokenizer_regex_stride_t* stride, uint16_t state) {
const uint8_t* bitmap = iree_tokenizer_regex_stride_boring_bitmap(stride);
return (bitmap[state / 8] & (1 << (state % 8))) != 0;
}
// Returns the self-loop category for a state, or 0xFF if no self-loop.
// A self-loop category C means: for all ASCII bytes in category C,
// transitions[state * 256 + byte] == state.
static inline uint8_t iree_tokenizer_regex_stride_self_loop_category(
const iree_tokenizer_regex_stride_t* stride, uint16_t state) {
const uint8_t* table =
(const uint8_t*)((uintptr_t)stride + stride->self_loop_category_offset);
return table[state];
}
// Allocates and computes stride acceleration data for a loaded DFA.
//
// Analyzes the DFA transition table to compute byte equivalence classes,
// boring state bitmap, and self-loop categories. The resulting stride is
// passed to exec_feed() to accelerate ASCII-heavy execution.
//
// |dfa| is the loaded DFA (must remain valid for the lifetime of the stride).
// |allocator| is used for the single slab allocation.
// |out_stride| receives the allocated stride (caller must free).
iree_status_t iree_tokenizer_regex_stride_allocate(
const iree_tokenizer_regex_dfa_t* dfa, iree_allocator_t allocator,
iree_tokenizer_regex_stride_t** out_stride);
// Frees stride resources.
void iree_tokenizer_regex_stride_free(iree_tokenizer_regex_stride_t* stride,
iree_allocator_t allocator);
//===----------------------------------------------------------------------===//
// Split Behavior (for pre-tokenization)
//===----------------------------------------------------------------------===//
// How to handle matched delimiters when splitting text.
// These match HuggingFace tokenizers' SplitDelimiterBehavior.
typedef enum iree_tokenizer_regex_split_behavior_e {
// Discard the matched delimiter entirely.
// "a]b" with pattern "]" -> ["a", "b"]
IREE_TOKENIZER_UTIL_REGEX_SPLIT_REMOVED = 0,
// Emit the delimiter as its own separate segment.
// "a]b" with pattern "]" -> ["a", "]", "b"]
IREE_TOKENIZER_UTIL_REGEX_SPLIT_ISOLATED = 1,
// Merge the delimiter with the previous segment.
// "a]b" with pattern "]" -> ["a]", "b"]
IREE_TOKENIZER_UTIL_REGEX_SPLIT_MERGED_WITH_PREVIOUS = 2,
// Merge the delimiter with the next segment.
// "a]b" with pattern "]" -> ["a", "]b"]
IREE_TOKENIZER_UTIL_REGEX_SPLIT_MERGED_WITH_NEXT = 3,
// Merge consecutive delimiters into a single segment.
// "a]]b" with pattern "]" -> ["a", "]]", "b"]
IREE_TOKENIZER_UTIL_REGEX_SPLIT_CONTIGUOUS = 4,
} iree_tokenizer_regex_split_behavior_t;
// Returns true if the split behavior creates BPE word boundaries.
//
// Word-splitting behaviors (REMOVED, ISOLATED, CONTIGUOUS) create segments that
// should be tokenized independently by the BPE model.
//
// Offset-tracking behaviors (MERGED_WITH_PREVIOUS, MERGED_WITH_NEXT) produce
// segments that are metadata for character-to-token mapping, NOT BPE
// boundaries. For these, the BPE model should see the full text as one unit.
//
// Note: When a ByteLevel transform follows a non-boundary Split in a Sequence,
// the ByteLevel conversion (space → Ġ) makes segments safe for independent
// tokenization because BPE was trained with Ġ at word starts.
static inline bool iree_tokenizer_regex_split_creates_bpe_boundaries(
iree_tokenizer_regex_split_behavior_t behavior) {
return behavior != IREE_TOKENIZER_UTIL_REGEX_SPLIT_MERGED_WITH_PREVIOUS &&
behavior != IREE_TOKENIZER_UTIL_REGEX_SPLIT_MERGED_WITH_NEXT;
}
//===----------------------------------------------------------------------===//
// DFA Loading
//===----------------------------------------------------------------------===//
// Loads a DFA from binary data.
//
// This is a zero-copy operation - the DFA structure points directly into the
// binary data. The caller must ensure |data| remains valid for the lifetime
// of |out_dfa|.
//
// |data| is the binary DFA data (must remain valid).
// |out_dfa| receives the loaded DFA handle.
//
// Returns:
// - IREE_STATUS_INVALID_ARGUMENT if data is too small or NULL.
// - IREE_STATUS_FAILED_PRECONDITION if magic number is invalid.
// - IREE_STATUS_UNIMPLEMENTED if version is unsupported.
iree_status_t iree_tokenizer_regex_dfa_load(
iree_const_byte_span_t data, iree_tokenizer_regex_dfa_t* out_dfa);
// Validates a loaded DFA for internal consistency.
//
// This performs additional validation beyond load-time checks:
// - Verifies accepting state count matches bitmap population count
// - Checks all transitions reference valid states or NO_TRANSITION
// - Validates lookahead data if present
//
// Returns IREE_STATUS_DATA_LOSS if internal inconsistency detected.
iree_status_t iree_tokenizer_regex_dfa_validate(
const iree_tokenizer_regex_dfa_t* dfa);
//===----------------------------------------------------------------------===//
// DFA Inspection
//===----------------------------------------------------------------------===//
// Returns the number of states in the DFA.
static inline uint16_t iree_tokenizer_regex_dfa_state_count(
const iree_tokenizer_regex_dfa_t* dfa) {
return dfa->header->num_states;
}
// Returns the DFA flags.
static inline iree_tokenizer_regex_dfa_flags_t iree_tokenizer_regex_dfa_flags(
const iree_tokenizer_regex_dfa_t* dfa) {
return dfa->header->flags;
}
// Returns the start state index.
static inline uint16_t iree_tokenizer_regex_dfa_start_state(
const iree_tokenizer_regex_dfa_t* dfa) {
return dfa->header->start_state;
}
// Returns true if the DFA uses Unicode pseudo-bytes.
static inline bool iree_tokenizer_regex_dfa_uses_unicode(
const iree_tokenizer_regex_dfa_t* dfa) {
return (dfa->header->flags & IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_UNICODE) != 0;
}
// Returns true if state |index| is an accepting state.
//
// Branch-free implementation for hot path performance. The state parameter
// comes from dfa_step() which only returns valid states or NO_TRANSITION.
// Callers check for NO_TRANSITION before calling this function, so state is
// always valid in the hot path. DFA validation
// (iree_tokenizer_regex_dfa_validate) ensures all states reference valid
// indices at load time.
static inline bool iree_tokenizer_regex_dfa_is_accepting(
const iree_tokenizer_regex_dfa_t* dfa, uint16_t state) {
uint64_t word = dfa->accepting_bitmap[state / 64];
return (word >> (state % 64)) & 1;
}
//===----------------------------------------------------------------------===//
// Step Function (lowest level)
//===----------------------------------------------------------------------===//
// Performs a single DFA transition.
//
// Checks exact codepoint range transitions first (for patterns like [一-龥]),
// then falls back to byte/pseudo-byte transitions.
//
// |codepoint| is the Unicode codepoint (or byte value for validation).
// |byte| is the category pseudo-byte for |codepoint| (from codepoint_to_byte).
//
// For validation/iteration over the byte table, pass (byte, byte) - the range
// check will not match (ranges are for codepoints > 255) and falls through.
//
// Returns the next state or IREE_TOKENIZER_UTIL_REGEX_NO_TRANSITION.
static inline uint16_t iree_tokenizer_regex_dfa_step(
const iree_tokenizer_regex_dfa_t* dfa, uint16_t current_state,
uint32_t codepoint, uint8_t byte) {
// Check exact codepoint range transitions first.
// Ranges are sorted by start codepoint for early exit.
if (dfa->num_ranges > 0) {
for (uint16_t i = 0; i < dfa->num_ranges; ++i) {
const iree_tokenizer_regex_dfa_range_transition_t* range =
&dfa->ranges[i];
if (range->from_state != current_state) continue;
if (codepoint < range->start) continue; // Before this range.
if (codepoint <= range->end) {
return range->target_state; // Exact match!
}
}
}
// Fall back to byte/pseudo-byte transition.
return dfa->transitions[(iree_host_size_t)current_state * 256 + byte];
}
//===----------------------------------------------------------------------===//
// Streaming Executor
//===----------------------------------------------------------------------===//
// Initializes execution state for a new matching session.
//
// |state| is caller-owned storage for execution state (typically stack).
// |dfa| is the DFA to execute (must remain valid during execution).
void iree_tokenizer_regex_exec_initialize(
iree_tokenizer_regex_exec_state_t* state,
const iree_tokenizer_regex_dfa_t* dfa);
// Feeds a chunk of UTF-8 text to the executor, emitting matches as found.
//
// This implements leftmost-longest matching semantics:
// - Matches start as early as possible in the text
// - Among matches starting at the same position, the longest is chosen
// - After a match, scanning resumes from the end of the match
//
// Matches are emitted via |callback| with positions relative to the overall
// stream (using |base_offset| + local position).
//
// |dfa| is the DFA to execute.
// |state| is the execution state (from exec_init, persists across calls).
// |chunk| is UTF-8 text to process. Multi-byte sequences must not be split
// across chunks (callers handle UTF-8 boundary buffering).
// |base_offset| is the byte offset of this chunk in the overall stream.
// |stride| is optional precomputed acceleration data (NULL to disable).
// |callback| is invoked for each complete match found (may be NULL).
// |user_data| is passed to the callback.
//
// Returns status from callback if it returns non-OK.
iree_status_t iree_tokenizer_regex_exec_feed(
const iree_tokenizer_regex_dfa_t* dfa,
iree_tokenizer_regex_exec_state_t* state, iree_string_view_t chunk,
iree_host_size_t base_offset, const iree_tokenizer_regex_stride_t* stride,
iree_tokenizer_regex_match_callback_fn_t callback, void* user_data);
// Finalizes execution, flushing any pending match at end of input.
//
// Must be called after all chunks have been fed to ensure the final match
// (if any) is emitted. After this call, the state should not be reused
// without calling exec_init again.
//
// |dfa| is the DFA that was executed.
// |state| is the execution state.
// |total_length| is the total length of all input fed (for match end position).
// |callback| is invoked if there's a pending match (may be NULL).
// |user_data| is passed to the callback.
//
// Returns status from callback if it returns non-OK.
iree_status_t iree_tokenizer_regex_exec_finalize(
const iree_tokenizer_regex_dfa_t* dfa,
iree_tokenizer_regex_exec_state_t* state, iree_host_size_t total_length,
iree_tokenizer_regex_match_callback_fn_t callback, void* user_data);
//===----------------------------------------------------------------------===//
// Convenience Wrappers
//===----------------------------------------------------------------------===//
// Executes the DFA on complete text, emitting matches via callback.
//
// This is a thin wrapper around init/feed/finalize for the common case
// where complete text is available.
//
// |dfa| is the DFA to execute.
// |text| is the complete UTF-8 input text.
// |callback| is invoked for each match (may be NULL to just validate).
// |user_data| is passed to the callback.
//
// Returns status from callback if it returns non-OK.
iree_status_t iree_tokenizer_regex_exec(
const iree_tokenizer_regex_dfa_t* dfa, iree_string_view_t text,
iree_tokenizer_regex_match_callback_fn_t callback, void* user_data);
// Counts matches without collecting them.
//
// More efficient than exec() with a counting callback when only the count
// is needed.
//
// |dfa| is the DFA to execute.
// |text| is the UTF-8 input text.
// |out_count| receives the number of matches found.
iree_status_t iree_tokenizer_regex_count_matches(
const iree_tokenizer_regex_dfa_t* dfa, iree_string_view_t text,
iree_host_size_t* out_count);
// Checks if the pattern matches anywhere in the text.
//
// Short-circuits on first match - more efficient than counting when you
// only need to know if any match exists.
//
// |dfa| is the DFA to execute.
// |text| is the UTF-8 input text.
// |out_has_match| receives true if at least one match was found.
iree_status_t iree_tokenizer_regex_has_match(
const iree_tokenizer_regex_dfa_t* dfa, iree_string_view_t text,
bool* out_has_match);
//===----------------------------------------------------------------------===//
// Binary Size Calculation
//===----------------------------------------------------------------------===//
// Calculates the minimum binary size for a DFA with the given parameters.
// Useful for pre-allocating buffers when building DFAs.
//
// |num_states| is the number of states.
// |flags| specifies which optional sections are included.
// |num_ranges| is the number of range transitions (0 if HAS_RANGES not set).
static inline iree_host_size_t iree_tokenizer_regex_dfa_binary_size(
uint16_t num_states, iree_tokenizer_regex_dfa_flags_t flags,
uint16_t num_ranges) {
iree_host_size_t bitmap_qwords =
iree_host_align(num_states, 64) / 64; // ceil(num_states / 64)
iree_host_size_t size = sizeof(iree_tokenizer_regex_dfa_header_t);
size +=
(iree_host_size_t)num_states * 256 * sizeof(uint16_t); // Transitions.
size += bitmap_qwords * sizeof(uint64_t); // Accept bitmap.
if (flags & IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_LOOKAHEAD) {
size += num_states * sizeof(iree_tokenizer_regex_lookahead_t);
}
if (flags & IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_ANCHORS) {
size += 2 * bitmap_qwords * sizeof(uint64_t); // Anchor bitmaps.
}
if (flags & IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_BRANCHES) {
// alive_branches and accepting_branches arrays.
size += 2 * (iree_host_size_t)num_states * sizeof(uint64_t);
}
if (flags & IREE_TOKENIZER_UTIL_REGEX_DFA_FLAG_HAS_RANGES) {
// Range header (num_ranges + reserved) + range array.
size += sizeof(uint32_t); // Header: uint16_t num_ranges + uint16_t pad.
size += num_ranges * sizeof(iree_tokenizer_regex_dfa_range_transition_t);
}
return size;
}
#ifdef __cplusplus
} // extern "C"
#endif
#endif // IREE_TOKENIZER_UTIL_REGEX_EXEC_H_