| // 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 | |
| #include "iree/tokenizer/regex/compile.h" | |
| #include "iree/base/internal/arena.h" | |
| #include "iree/tokenizer/regex/internal/dfa.h" | |
| #include "iree/tokenizer/regex/internal/nfa.h" | |
| #include "iree/tokenizer/regex/internal/parser.h" | |
| //===----------------------------------------------------------------------===// | |
| // AST Helpers | |
| //===----------------------------------------------------------------------===// | |
| // Recursively sets case_insensitive=true on all nodes in the AST. | |
| // Used to propagate the global CASE_INSENSITIVE flag to all nodes. | |
| static void iree_tokenizer_regex_ast_propagate_case_insensitive( | |
| iree_tokenizer_regex_ast_node_t* node) { | |
| if (!node) return; | |
| node->case_insensitive = true; | |
| switch (node->type) { | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_EMPTY: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_LITERAL: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_DOT: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_CHAR_CLASS: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_SHORTHAND: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_UNICODE_PROP: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_ANCHOR_START: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_ANCHOR_END: | |
| break; // Leaf nodes. | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_CONCAT: | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_ALTERNATION: | |
| for (iree_host_size_t i = 0; i < node->data.compound.child_count; ++i) { | |
| iree_tokenizer_regex_ast_propagate_case_insensitive( | |
| node->data.compound.children[i]); | |
| } | |
| break; | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_QUANTIFIER: | |
| iree_tokenizer_regex_ast_propagate_case_insensitive( | |
| node->data.quantifier.child); | |
| break; | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_GROUP: | |
| iree_tokenizer_regex_ast_propagate_case_insensitive( | |
| node->data.group_child); | |
| break; | |
| case IREE_TOKENIZER_UTIL_REGEX_AST_NEG_LOOKAHEAD: | |
| iree_tokenizer_regex_ast_propagate_case_insensitive( | |
| node->data.lookahead_child); | |
| break; | |
| } | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Compilation Pipeline | |
| //===----------------------------------------------------------------------===// | |
| iree_status_t iree_tokenizer_regex_compile( | |
| iree_string_view_t pattern, iree_tokenizer_regex_compile_flags_t flags, | |
| iree_allocator_t allocator, uint8_t** out_dfa_data, | |
| iree_host_size_t* out_dfa_size, | |
| iree_tokenizer_regex_compile_error_t* out_error) { | |
| IREE_ASSERT_ARGUMENT(out_dfa_data); | |
| IREE_ASSERT_ARGUMENT(out_dfa_size); | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)pattern.size); | |
| *out_dfa_data = NULL; | |
| *out_dfa_size = 0; | |
| if (out_error) { | |
| out_error->position = 0; | |
| out_error->length = 0; | |
| out_error->message = NULL; | |
| } | |
| // Reject empty patterns (would create empty-match-everywhere DFA). | |
| if (pattern.size == 0) { | |
| if (out_error) { | |
| out_error->position = 0; | |
| out_error->message = "empty pattern not allowed"; | |
| } | |
| IREE_RETURN_AND_END_ZONE( | |
| z0, iree_make_status(IREE_STATUS_INVALID_ARGUMENT, "empty pattern")); | |
| } | |
| // Arena for all temporary allocations (AST, NFA, DFA construction). | |
| // This is freed at the end regardless of success/failure. | |
| iree_arena_block_pool_t block_pool; | |
| iree_arena_block_pool_initialize(/*block_size=*/16384, allocator, | |
| &block_pool); | |
| iree_arena_allocator_t arena; | |
| iree_arena_initialize(&block_pool, &arena); | |
| // Stage 1: Parse pattern to AST. | |
| iree_tokenizer_regex_ast_node_t* ast = NULL; | |
| iree_tokenizer_regex_parse_error_t parse_error = {0}; | |
| iree_status_t status = | |
| iree_tokenizer_regex_parse(pattern, &arena, &ast, &parse_error); | |
| if (!iree_status_is_ok(status) && out_error) { | |
| out_error->position = parse_error.position; | |
| out_error->length = parse_error.length; | |
| out_error->message = parse_error.message; | |
| } | |
| // Apply global case-insensitive flag if set. | |
| // Must propagate to all nodes, not just root, since NFA builder reads | |
| // the flag from each individual node. | |
| if (iree_status_is_ok(status) && | |
| (flags & IREE_TOKENIZER_UTIL_REGEX_COMPILE_FLAG_CASE_INSENSITIVE)) { | |
| iree_tokenizer_regex_ast_propagate_case_insensitive(ast); | |
| } | |
| // Stage 2: Build NFA from AST (Thompson's construction). | |
| iree_tokenizer_regex_nfa_t nfa = {0}; | |
| if (iree_status_is_ok(status)) { | |
| status = iree_tokenizer_regex_nfa_build(ast, &arena, &nfa); | |
| if (!iree_status_is_ok(status) && out_error) { | |
| out_error->message = "NFA construction failed"; | |
| } | |
| } | |
| // Stage 3: Build DFA from NFA (subset construction). | |
| iree_tokenizer_regex_dfa_build_t dfa = {0}; | |
| if (iree_status_is_ok(status)) { | |
| status = iree_tokenizer_regex_dfa_build(&nfa, &arena, &dfa); | |
| if (!iree_status_is_ok(status) && out_error) { | |
| out_error->message = "DFA construction failed (state limit?)"; | |
| } | |
| } | |
| // Stage 3.5: Validate DFA doesn't match empty strings. | |
| // | |
| // The start state being accepting (without end anchor) means the pattern | |
| // can match empty strings at every position, which would cause infinite | |
| // loops in the executor. Patterns like a*, a?, (a|)* fall into this category. | |
| // | |
| // Patterns with end anchor like ^.*$ are safe because acceptance is deferred | |
| // until end of input - no intermediate empty matches occur. | |
| if (iree_status_is_ok(status) && dfa.states[0]->is_accepting && | |
| !dfa.states[0]->requires_end_anchor) { | |
| if (out_error) { | |
| out_error->position = 0; | |
| out_error->message = "pattern matches empty string"; | |
| } | |
| status = iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "pattern matches empty string (use + instead of * for required match)"); | |
| } | |
| // Additional check: if start is accepting with end anchor but no transitions, | |
| // it can only match empty strings (e.g., ^$). | |
| if (iree_status_is_ok(status) && dfa.states[0]->is_accepting && | |
| dfa.states[0]->requires_end_anchor) { | |
| bool has_transition = false; | |
| for (int byte = 0; byte < 256 && !has_transition; ++byte) { | |
| if (dfa.states[0]->transitions[byte] != UINT32_MAX) { | |
| has_transition = true; | |
| } | |
| } | |
| if (!has_transition) { | |
| if (out_error) { | |
| out_error->position = 0; | |
| out_error->message = "pattern can only match empty string"; | |
| } | |
| status = iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "pattern can only match empty strings"); | |
| } | |
| } | |
| // Stage 4: Serialize DFA to binary format. | |
| if (iree_status_is_ok(status)) { | |
| status = iree_tokenizer_regex_dfa_serialize(&dfa, allocator, out_dfa_data, | |
| out_dfa_size); | |
| if (!iree_status_is_ok(status) && out_error) { | |
| out_error->message = "DFA serialization failed"; | |
| } | |
| } | |
| // Free all temporary allocations. | |
| iree_arena_deinitialize(&arena); | |
| iree_arena_block_pool_deinitialize(&block_pool); | |
| IREE_TRACE_ZONE_END(z0); | |
| return status; | |
| } | |
| iree_status_t iree_tokenizer_regex_compile_and_load( | |
| iree_string_view_t pattern, iree_tokenizer_regex_compile_flags_t flags, | |
| iree_allocator_t allocator, iree_tokenizer_regex_dfa_t* out_dfa, | |
| uint8_t** out_dfa_storage, | |
| iree_tokenizer_regex_compile_error_t* out_error) { | |
| IREE_ASSERT_ARGUMENT(out_dfa); | |
| IREE_ASSERT_ARGUMENT(out_dfa_storage); | |
| *out_dfa_storage = NULL; | |
| memset(out_dfa, 0, sizeof(*out_dfa)); | |
| // Compile. | |
| uint8_t* dfa_data = NULL; | |
| iree_host_size_t dfa_size = 0; | |
| IREE_RETURN_IF_ERROR(iree_tokenizer_regex_compile( | |
| pattern, flags, allocator, &dfa_data, &dfa_size, out_error)); | |
| // Load. | |
| iree_status_t status = iree_tokenizer_regex_dfa_load( | |
| iree_make_const_byte_span(dfa_data, dfa_size), out_dfa); | |
| if (!iree_status_is_ok(status)) { | |
| iree_allocator_free(allocator, dfa_data); | |
| return status; | |
| } | |
| *out_dfa_storage = dfa_data; | |
| return iree_ok_status(); | |
| } |