blob: 93eb27e7d6f428fa08fa63d8250680395cc027ba [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
#include "iree/tokenizer/normalizer/regex_replace.h"
#include <string.h>
#include "iree/tokenizer/regex/compile.h"
#include "iree/tokenizer/regex/exec.h"
// Normalizer struct. Content data follows immediately after this struct.
typedef struct iree_tokenizer_normalizer_regex_replace_t {
iree_tokenizer_normalizer_t base;
iree_allocator_t allocator;
iree_tokenizer_regex_dfa_t dfa;
iree_tokenizer_regex_stride_t* stride;
uint8_t* dfa_data;
uint8_t content_length;
// Trailing data: [content_data]
} iree_tokenizer_normalizer_regex_replace_t;
// State structure. Uses inline callback processing like split.c.
typedef struct iree_tokenizer_normalizer_regex_replace_state_t {
iree_tokenizer_normalizer_state_t base;
// Position tracking (all absolute byte offsets).
// Total bytes fed to regex.
iree_host_size_t bytes_processed;
// Bytes committed to output stream.
iree_host_size_t output_position;
// Pending match: when we've consumed a match's input but haven't finished
// emitting its replacement content. This happens when output buffer fills
// mid-replacement.
// End of match needing content emission.
iree_host_size_t pending_match_end;
// True if content emission is pending.
bool has_pending_match;
// Partial content emission (when output smaller than content).
uint8_t content_emitted;
// Cached from normalizer for hot path.
const uint8_t* content;
uint8_t content_length;
// Set after finalize's regex phase completes.
bool finalize_done;
// Regex execution state.
iree_tokenizer_regex_exec_state_t regex_state;
} iree_tokenizer_normalizer_regex_replace_state_t;
static const iree_tokenizer_normalizer_vtable_t
iree_tokenizer_normalizer_regex_replace_vtable;
iree_status_t iree_tokenizer_normalizer_regex_replace_allocate(
iree_string_view_t pattern, iree_string_view_t content,
iree_allocator_t allocator, iree_tokenizer_normalizer_t** out_normalizer) {
IREE_TRACE_ZONE_BEGIN(z0);
IREE_ASSERT_ARGUMENT(out_normalizer);
*out_normalizer = NULL;
// Validate inputs before any allocations.
if (pattern.size == 0) {
IREE_RETURN_AND_END_ZONE(
z0, iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
"regex replace normalizer requires non-empty "
"pattern"));
}
if (content.size > IREE_TOKENIZER_REGEX_REPLACE_MAX_CONTENT) {
IREE_RETURN_AND_END_ZONE(
z0,
iree_make_status(
IREE_STATUS_INVALID_ARGUMENT,
"regex replace content exceeds maximum length (%" PRIhsz " > %d)",
content.size, IREE_TOKENIZER_REGEX_REPLACE_MAX_CONTENT));
}
// Calculate total allocation size.
iree_host_size_t total_size = 0;
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, IREE_STRUCT_LAYOUT(
iree_sizeof_struct(iree_tokenizer_normalizer_regex_replace_t),
&total_size, IREE_STRUCT_FIELD(content.size, uint8_t, NULL)));
// Compile regex pattern (first allocation — status chaining from here).
iree_tokenizer_regex_dfa_t dfa = {0};
uint8_t* dfa_data = NULL;
iree_tokenizer_regex_stride_t* stride = NULL;
iree_tokenizer_normalizer_regex_replace_t* normalizer = NULL;
iree_tokenizer_regex_compile_error_t compile_error;
iree_status_t status = iree_tokenizer_regex_compile_and_load(
pattern, IREE_TOKENIZER_UTIL_REGEX_COMPILE_FLAG_NONE, allocator, &dfa,
&dfa_data, &compile_error);
if (!iree_status_is_ok(status) && compile_error.message[0] != '\0') {
status =
iree_status_annotate_f(status, "regex compile error at %zu: %s",
compile_error.position, compile_error.message);
}
if (iree_status_is_ok(status)) {
status = iree_tokenizer_regex_stride_allocate(&dfa, allocator, &stride);
}
if (iree_status_is_ok(status)) {
status = iree_allocator_malloc(allocator, total_size, (void**)&normalizer);
}
if (iree_status_is_ok(status)) {
iree_tokenizer_normalizer_initialize(
&normalizer->base, &iree_tokenizer_normalizer_regex_replace_vtable,
sizeof(iree_tokenizer_normalizer_regex_replace_state_t));
normalizer->allocator = allocator;
normalizer->dfa = dfa;
normalizer->stride = stride;
normalizer->dfa_data = dfa_data;
normalizer->content_length = (uint8_t)content.size;
if (content.size > 0) {
memcpy((uint8_t*)(normalizer + 1), content.data, content.size);
}
*out_normalizer = &normalizer->base;
} else {
iree_tokenizer_regex_stride_free(stride, allocator);
iree_allocator_free(allocator, dfa_data);
}
IREE_TRACE_ZONE_END(z0);
return status;
}
static inline const uint8_t* iree_tokenizer_normalizer_regex_replace_content(
const iree_tokenizer_normalizer_regex_replace_t* normalizer) {
return (const uint8_t*)(normalizer + 1);
}
static void iree_tokenizer_normalizer_regex_replace_destroy(
iree_tokenizer_normalizer_t* normalizer) {
IREE_TRACE_ZONE_BEGIN(z0);
iree_tokenizer_normalizer_regex_replace_t* self =
(iree_tokenizer_normalizer_regex_replace_t*)normalizer;
iree_allocator_t allocator = self->allocator;
if (self->stride) {
iree_tokenizer_regex_stride_free(self->stride, allocator);
}
if (self->dfa_data) {
iree_allocator_free(allocator, self->dfa_data);
}
iree_allocator_free(allocator, self);
IREE_TRACE_ZONE_END(z0);
}
static iree_status_t iree_tokenizer_normalizer_regex_replace_state_initialize(
const iree_tokenizer_normalizer_t* normalizer, void* storage,
iree_tokenizer_normalizer_state_t** out_state) {
IREE_TRACE_ZONE_BEGIN(z0);
const iree_tokenizer_normalizer_regex_replace_t* self =
(const iree_tokenizer_normalizer_regex_replace_t*)normalizer;
iree_tokenizer_normalizer_regex_replace_state_t* state =
(iree_tokenizer_normalizer_regex_replace_state_t*)storage;
memset(state, 0, sizeof(*state));
state->base.normalizer = normalizer;
state->content = iree_tokenizer_normalizer_regex_replace_content(self);
state->content_length = self->content_length;
iree_tokenizer_regex_exec_initialize(&state->regex_state, &self->dfa);
*out_state = &state->base;
IREE_TRACE_ZONE_END(z0);
return iree_ok_status();
}
static void iree_tokenizer_normalizer_regex_replace_state_deinitialize(
iree_tokenizer_normalizer_state_t* state) {
IREE_TRACE_ZONE_BEGIN(z0);
(void)state;
IREE_TRACE_ZONE_END(z0);
}
// Callback context for inline match processing.
typedef struct iree_tokenizer_regex_replace_context_t {
// Input/output pointers.
const uint8_t* input_base;
iree_host_size_t chunk_base;
uint8_t** out_ptr;
uint8_t* out_end;
// Mutable state (updated per-match on successful emit).
iree_host_size_t output_position;
uint8_t content_emitted;
// Pending match: set when we start emitting content but can't finish.
// output_position stays at match.start; pending_match_end records match.end.
iree_host_size_t pending_match_end;
bool has_pending_match;
// Immutable config.
const uint8_t* content;
uint8_t content_length;
} iree_tokenizer_regex_replace_context_t;
// Emits bytes from input that aren't part of a match (passthrough).
// Returns the number of bytes written.
static iree_host_size_t iree_tokenizer_regex_replace_emit_passthrough(
iree_tokenizer_regex_replace_context_t* context,
iree_host_size_t target_position) {
if (target_position <= context->output_position) {
return 0;
}
iree_host_size_t to_copy = target_position - context->output_position;
iree_host_size_t output_available =
(iree_host_size_t)(context->out_end - *context->out_ptr);
iree_host_size_t to_write =
to_copy < output_available ? to_copy : output_available;
if (to_write > 0) {
iree_host_size_t input_offset =
context->output_position - context->chunk_base;
memcpy(*context->out_ptr, context->input_base + input_offset, to_write);
*context->out_ptr += to_write;
context->output_position += to_write;
}
return to_write;
}
// Emits replacement content. Returns true if all content was emitted.
static bool iree_tokenizer_regex_replace_emit_content(
iree_tokenizer_regex_replace_context_t* context) {
if (context->content_length == 0) {
return true;
}
iree_host_size_t output_available =
(iree_host_size_t)(context->out_end - *context->out_ptr);
iree_host_size_t remaining =
context->content_length - context->content_emitted;
iree_host_size_t to_write =
remaining < output_available ? remaining : output_available;
if (to_write > 0) {
memcpy(*context->out_ptr, context->content + context->content_emitted,
to_write);
*context->out_ptr += to_write;
context->content_emitted += (uint8_t)to_write;
}
if (context->content_emitted >= context->content_length) {
context->content_emitted = 0;
return true;
}
return false;
}
// Inline callback: processes each match immediately.
// Emits passthrough bytes up to match start, then emits replacement content.
// Returns RESOURCE_EXHAUSTED if output buffer is full.
static iree_status_t iree_tokenizer_regex_replace_match_callback(
void* user_data, iree_tokenizer_regex_match_t match) {
iree_tokenizer_regex_replace_context_t* context =
(iree_tokenizer_regex_replace_context_t*)user_data;
// Emit passthrough bytes from output_position to match.start.
iree_tokenizer_regex_replace_emit_passthrough(context, match.start);
if (context->output_position < match.start) {
return iree_status_from_code(IREE_STATUS_RESOURCE_EXHAUSTED);
}
// Emit replacement content.
if (!iree_tokenizer_regex_replace_emit_content(context)) {
// Partial or no content written. Record the pending match so we can finish
// emitting content and then advance to match.end on resume.
// CRITICAL: Do NOT advance output_position here - it stays at match.start
// so we know content emission is pending.
context->has_pending_match = true;
context->pending_match_end = match.end;
return iree_status_from_code(IREE_STATUS_RESOURCE_EXHAUSTED);
}
// Advance past matched region.
context->output_position = match.end;
return iree_ok_status();
}
static iree_status_t iree_tokenizer_normalizer_regex_replace_state_process(
iree_tokenizer_normalizer_state_t* base_state, iree_string_view_t input,
iree_mutable_string_view_t output, iree_tokenizer_normalizer_flags_t flags,
iree_host_size_t* IREE_RESTRICT out_consumed,
iree_host_size_t* IREE_RESTRICT out_written) {
(void)flags; // Regex replace handles boundaries via exec state.
iree_tokenizer_normalizer_regex_replace_state_t* state =
(iree_tokenizer_normalizer_regex_replace_state_t*)base_state;
const iree_tokenizer_normalizer_regex_replace_t* normalizer =
(const iree_tokenizer_normalizer_regex_replace_t*)base_state->normalizer;
uint8_t* out_ptr = (uint8_t*)output.data;
uint8_t* out_end = out_ptr + output.size;
iree_host_size_t chunk_base = state->bytes_processed;
*out_consumed = 0;
*out_written = 0;
if (input.size == 0 || output.size == 0) {
return iree_ok_status();
}
// Resume partial content emission from previous call.
if (state->has_pending_match) {
// We have a pending match: content emission was interrupted.
// content_emitted tracks how much was written; may be 0 if output was full.
iree_host_size_t remaining = state->content_length - state->content_emitted;
iree_host_size_t output_available = (iree_host_size_t)(out_end - out_ptr);
iree_host_size_t to_write =
remaining < output_available ? remaining : output_available;
if (to_write > 0) {
memcpy(out_ptr, state->content + state->content_emitted, to_write);
out_ptr += to_write;
state->content_emitted += (uint8_t)to_write;
}
if (state->content_emitted < state->content_length) {
// Still more content to emit; output full.
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
// Content fully emitted. Now advance output_position past the match.
state->output_position = state->pending_match_end;
state->content_emitted = 0;
state->has_pending_match = false;
}
// Set up callback context.
iree_tokenizer_regex_replace_context_t context = {
.input_base = (const uint8_t*)input.data,
.chunk_base = chunk_base,
.out_ptr = &out_ptr,
.out_end = out_end,
.output_position = state->output_position,
.content_emitted = 0,
.pending_match_end = 0,
.has_pending_match = false,
.content = state->content,
.content_length = state->content_length,
};
// Feed regex; matches are processed inline via callback.
iree_status_t feed_status = iree_tokenizer_regex_exec_feed(
&normalizer->dfa, &state->regex_state, input, chunk_base,
normalizer->stride, iree_tokenizer_regex_replace_match_callback,
&context);
// Handle feed result.
if (!iree_status_is_ok(feed_status) &&
!iree_status_is_resource_exhausted(feed_status)) {
return feed_status;
}
bool output_full = iree_status_is_resource_exhausted(feed_status);
iree_status_ignore(feed_status);
iree_host_size_t input_end = chunk_base + input.size;
if (output_full) {
// Output filled mid-chunk. Save state for resume.
state->output_position = context.output_position;
state->content_emitted = context.content_emitted;
state->has_pending_match = context.has_pending_match;
state->pending_match_end = context.pending_match_end;
// Reset regex for re-entry. If we have a pending match, the match's input
// has been consumed but content emission was interrupted. Regex resumes
// from pending_match_end (after the match). Otherwise, resume from
// output_position.
iree_host_size_t resume_position = context.has_pending_match
? context.pending_match_end
: context.output_position;
state->bytes_processed = resume_position;
iree_tokenizer_regex_exec_initialize(&state->regex_state, &normalizer->dfa);
*out_consumed = resume_position - chunk_base;
} else {
// Normal case: all input was scanned. Emit passthrough up to confirmed end.
// If regex is tracking a partial match, only emit up to match_start.
iree_host_size_t confirmed_end = state->regex_state.in_match
? state->regex_state.match_start
: input_end;
iree_tokenizer_regex_replace_emit_passthrough(&context, confirmed_end);
// Only consume up to what we actually emitted. If output was full during
// passthrough, output_position will be less than confirmed_end, and we
// need to re-process the remaining bytes next call.
state->output_position = context.output_position;
if (context.output_position < confirmed_end) {
// Output filled during final passthrough. Re-feed from output_position.
state->bytes_processed = context.output_position;
iree_tokenizer_regex_exec_initialize(&state->regex_state,
&normalizer->dfa);
*out_consumed = context.output_position - chunk_base;
} else {
// All confirmed bytes emitted. Consume all input.
state->bytes_processed = input_end;
*out_consumed = input.size;
}
}
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
static iree_status_t iree_tokenizer_normalizer_regex_replace_state_finalize(
iree_tokenizer_normalizer_state_t* base_state,
iree_mutable_string_view_t output,
iree_host_size_t* IREE_RESTRICT out_written) {
iree_tokenizer_normalizer_regex_replace_state_t* state =
(iree_tokenizer_normalizer_regex_replace_state_t*)base_state;
const iree_tokenizer_normalizer_regex_replace_t* normalizer =
(const iree_tokenizer_normalizer_regex_replace_t*)base_state->normalizer;
uint8_t* out_ptr = (uint8_t*)output.data;
uint8_t* out_end = out_ptr + output.size;
*out_written = 0;
// Resume partial content emission from pending match.
if (state->has_pending_match) {
iree_host_size_t remaining = state->content_length - state->content_emitted;
iree_host_size_t output_available = (iree_host_size_t)(out_end - out_ptr);
iree_host_size_t to_write =
remaining < output_available ? remaining : output_available;
if (to_write > 0) {
memcpy(out_ptr, state->content + state->content_emitted, to_write);
out_ptr += to_write;
state->content_emitted += (uint8_t)to_write;
}
if (state->content_emitted < state->content_length) {
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
// Content fully emitted. Advance past the match.
state->output_position = state->pending_match_end;
state->content_emitted = 0;
state->has_pending_match = false;
}
if (!state->finalize_done) {
iree_tokenizer_regex_replace_context_t context = {
.input_base = NULL, // No input for passthrough in callback.
.chunk_base = state->bytes_processed,
.out_ptr = &out_ptr,
.out_end = out_end,
.output_position = state->output_position,
.content_emitted = 0,
.pending_match_end = 0,
.has_pending_match = false,
.content = state->content,
.content_length = state->content_length,
};
// Finalize regex (may produce one final match at end-of-input).
iree_status_t finalize_status = iree_tokenizer_regex_exec_finalize(
&normalizer->dfa, &state->regex_state, state->bytes_processed,
iree_tokenizer_regex_replace_match_callback, &context);
if (iree_status_is_ok(finalize_status)) {
state->finalize_done = true;
state->output_position = context.output_position;
state->content_emitted = context.content_emitted;
state->has_pending_match = context.has_pending_match;
state->pending_match_end = context.pending_match_end;
} else if (iree_status_is_resource_exhausted(finalize_status)) {
// Output full. Commit what we have and return for re-entry.
// Mark finalize_done so re-entry skips exec_finalize and resumes content
// emission. Without this, exec_finalize would be called again,
// potentially double-emitting matches.
state->finalize_done = true;
state->output_position = context.output_position;
state->content_emitted = context.content_emitted;
state->has_pending_match = context.has_pending_match;
state->pending_match_end = context.pending_match_end;
iree_status_ignore(finalize_status);
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
} else {
return finalize_status;
}
}
// Emit trailing passthrough bytes that process() couldn't emit because the
// regex DFA was evaluating a partial match. The gap between output_position
// and bytes_processed represents bytes consumed by process() but held by the
// DFA. After exec_finalize resolves the partial match (as non-matching), the
// bytes must be emitted as passthrough. The data is in the regex exec state's
// rewind buffer, which exec_feed populates for partial matches and
// exec_finalize does not modify.
if (state->output_position < state->bytes_processed) {
iree_host_size_t gap = state->bytes_processed - state->output_position;
if (state->output_position < state->regex_state.rewind_buffer_start ||
state->output_position - state->regex_state.rewind_buffer_start + gap >
state->regex_state.rewind_buffer_length) {
return iree_make_status(
IREE_STATUS_OUT_OF_RANGE,
"regex replace finalize: trailing passthrough [%" PRIhsz ", %" PRIhsz
") not in rewind buffer [%" PRIhsz ", %" PRIhsz ")",
state->output_position, state->bytes_processed,
state->regex_state.rewind_buffer_start,
state->regex_state.rewind_buffer_start +
state->regex_state.rewind_buffer_length);
}
iree_host_size_t rewind_offset =
state->output_position - state->regex_state.rewind_buffer_start;
iree_host_size_t output_available = (iree_host_size_t)(out_end - out_ptr);
iree_host_size_t to_write = gap < output_available ? gap : output_available;
if (to_write > 0) {
memcpy(out_ptr, state->regex_state.rewind_buffer + rewind_offset,
to_write);
out_ptr += to_write;
state->output_position += to_write;
}
}
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
static bool iree_tokenizer_normalizer_regex_replace_state_has_pending(
const iree_tokenizer_normalizer_state_t* base_state) {
const iree_tokenizer_normalizer_regex_replace_state_t* state =
(const iree_tokenizer_normalizer_regex_replace_state_t*)base_state;
if (state->has_pending_match || state->content_emitted > 0) return true;
// Trailing passthrough gap: process() consumed bytes the regex DFA was
// evaluating but couldn't emit. After finalize resolves the partial match,
// the gap bytes are emitted from the rewind buffer.
if (state->output_position < state->bytes_processed) return true;
return false;
}
static const iree_tokenizer_normalizer_vtable_t
iree_tokenizer_normalizer_regex_replace_vtable = {
.destroy = iree_tokenizer_normalizer_regex_replace_destroy,
.state_initialize =
iree_tokenizer_normalizer_regex_replace_state_initialize,
.state_deinitialize =
iree_tokenizer_normalizer_regex_replace_state_deinitialize,
.state_process = iree_tokenizer_normalizer_regex_replace_state_process,
.state_finalize =
iree_tokenizer_normalizer_regex_replace_state_finalize,
.state_has_pending =
iree_tokenizer_normalizer_regex_replace_state_has_pending,
};