blob: 0abe07f38c48d43e29f4f4b7db2d1a0a1a011e27 [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/nfd.h"
#include <string.h>
#include "iree/base/internal/unicode.h"
//===----------------------------------------------------------------------===//
// NFD Normalizer Implementation
//===----------------------------------------------------------------------===//
// Maximum combining sequence length (starter + combining marks).
// Unicode Stream-Safe Text Format (UAX #15 §12) limits combining character
// sequences to 30 trailing characters. We use 32 for margin.
// Inputs exceeding this are provably non-conformant Unicode text.
#define IREE_TOKENIZER_NFD_MAX_SEQUENCE 32
typedef struct iree_tokenizer_normalizer_nfd_t {
iree_tokenizer_normalizer_t base;
iree_allocator_t allocator;
} iree_tokenizer_normalizer_nfd_t;
// State for streaming NFD processing.
//
// Buffers a combining sequence (starter + combining marks) until the next
// starter arrives, then canonically orders and emits it (without composition).
typedef struct iree_tokenizer_normalizer_nfd_state_t {
iree_tokenizer_normalizer_state_t base;
// Combining sequence buffer: codepoints awaiting emission.
// Entry 0 is the starter (CCC=0), entries 1..count-1 are combining marks.
uint32_t sequence[IREE_TOKENIZER_NFD_MAX_SEQUENCE];
uint8_t sequence_count;
// Position within the ordered sequence during emit phase.
// When emit_position < sequence_count, we're in the middle of emitting.
uint8_t emit_position;
// Partial UTF-8 encoding for a codepoint that didn't fully fit in output.
uint8_t pending_utf8[4];
uint8_t pending_utf8_count;
uint8_t pending_utf8_position;
// Buffered decomposed codepoints from an input codepoint being processed.
// Needed because Hangul decomposes to multiple starters (CCC=0) and we may
// not be able to ingest all of them in one call due to output capacity.
uint32_t decomposed_buffer[4];
uint8_t decomposed_count;
uint8_t decomposed_position;
} iree_tokenizer_normalizer_nfd_state_t;
static const iree_tokenizer_normalizer_vtable_t
iree_tokenizer_normalizer_nfd_vtable;
iree_status_t iree_tokenizer_normalizer_nfd_allocate(
iree_allocator_t allocator, iree_tokenizer_normalizer_t** out_normalizer) {
IREE_TRACE_ZONE_BEGIN(z0);
IREE_ASSERT_ARGUMENT(out_normalizer);
*out_normalizer = NULL;
iree_tokenizer_normalizer_nfd_t* normalizer = NULL;
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, iree_allocator_malloc(allocator, sizeof(*normalizer),
(void**)&normalizer));
iree_tokenizer_normalizer_initialize(
&normalizer->base, &iree_tokenizer_normalizer_nfd_vtable,
sizeof(iree_tokenizer_normalizer_nfd_state_t));
normalizer->allocator = allocator;
*out_normalizer = &normalizer->base;
IREE_TRACE_ZONE_END(z0);
return iree_ok_status();
}
static void iree_tokenizer_normalizer_nfd_destroy(
iree_tokenizer_normalizer_t* normalizer) {
IREE_TRACE_ZONE_BEGIN(z0);
iree_tokenizer_normalizer_nfd_t* self =
(iree_tokenizer_normalizer_nfd_t*)normalizer;
iree_allocator_t allocator = self->allocator;
iree_allocator_free(allocator, self);
IREE_TRACE_ZONE_END(z0);
}
static iree_status_t iree_tokenizer_normalizer_nfd_state_initialize(
const iree_tokenizer_normalizer_t* normalizer, void* storage,
iree_tokenizer_normalizer_state_t** out_state) {
IREE_TRACE_ZONE_BEGIN(z0);
iree_tokenizer_normalizer_nfd_state_t* state =
(iree_tokenizer_normalizer_nfd_state_t*)storage;
memset(state, 0, sizeof(*state));
state->base.normalizer = normalizer;
*out_state = &state->base;
IREE_TRACE_ZONE_END(z0);
return iree_ok_status();
}
static void iree_tokenizer_normalizer_nfd_state_deinitialize(
iree_tokenizer_normalizer_state_t* state) {
IREE_TRACE_ZONE_BEGIN(z0);
(void)state;
IREE_TRACE_ZONE_END(z0);
}
//===----------------------------------------------------------------------===//
// Canonical Ordering and Emission Helpers
//===----------------------------------------------------------------------===//
// Applies canonical ordering (stable sort by CCC) to the combining sequence.
// Unlike NFC, we do not compose — just order the combining marks.
static void iree_tokenizer_nfd_order_sequence(
iree_tokenizer_normalizer_nfd_state_t* state) {
if (state->sequence_count <= 1) return;
// Canonical ordering: insertion sort combining marks by CCC.
// Stable: marks with equal CCC preserve their relative order.
// Entry 0 is normally the starter (CCC=0). For defective sequences
// (no preceding starter), entry 0 has CCC > 0 and must participate.
uint8_t sort_start = (iree_unicode_ccc(state->sequence[0]) > 0) ? 0 : 1;
for (uint8_t i = sort_start + 1; i < state->sequence_count; ++i) {
uint32_t codepoint = state->sequence[i];
uint8_t ccc = iree_unicode_ccc(codepoint);
uint8_t j = i;
while (j > sort_start && iree_unicode_ccc(state->sequence[j - 1]) > ccc) {
state->sequence[j] = state->sequence[j - 1];
--j;
}
state->sequence[j] = codepoint;
}
}
// Emits pending UTF-8 bytes to the output buffer.
static iree_host_size_t iree_tokenizer_nfd_emit_pending_utf8(
iree_tokenizer_normalizer_nfd_state_t* state, uint8_t* output,
iree_host_size_t output_capacity) {
iree_host_size_t available =
state->pending_utf8_count - state->pending_utf8_position;
iree_host_size_t to_write =
available < output_capacity ? available : output_capacity;
if (to_write > 0) {
memcpy(output, state->pending_utf8 + state->pending_utf8_position,
to_write);
state->pending_utf8_position += (uint8_t)to_write;
if (state->pending_utf8_position >= state->pending_utf8_count) {
state->pending_utf8_count = 0;
state->pending_utf8_position = 0;
}
}
return to_write;
}
// Emits a single codepoint to the output buffer.
static iree_host_size_t iree_tokenizer_nfd_emit_codepoint(
iree_tokenizer_normalizer_nfd_state_t* state, uint32_t codepoint,
uint8_t* output, iree_host_size_t output_capacity) {
int encoded_length = iree_unicode_utf8_encoded_length(codepoint);
IREE_ASSERT(encoded_length > 0,
"invalid codepoint U+%04X in NFD emit (internal bug)", codepoint);
if (encoded_length <= 0) return 0;
if ((iree_host_size_t)encoded_length <= output_capacity) {
iree_unicode_utf8_encode(codepoint, (char*)output);
return (iree_host_size_t)encoded_length;
}
// Doesn't fully fit - encode to pending_utf8, write what fits.
IREE_ASSERT(state->pending_utf8_count == 0 ||
state->pending_utf8_position >= state->pending_utf8_count,
"pending_utf8 overwritten before fully drained");
iree_unicode_utf8_encode(codepoint, (char*)state->pending_utf8);
state->pending_utf8_count = (uint8_t)encoded_length;
state->pending_utf8_position = 0;
return iree_tokenizer_nfd_emit_pending_utf8(state, output, output_capacity);
}
// Emits codepoints from the ordered sequence starting at emit_position.
static iree_host_size_t iree_tokenizer_nfd_emit_sequence(
iree_tokenizer_normalizer_nfd_state_t* state, uint8_t* output,
iree_host_size_t output_capacity) {
uint8_t* out_ptr = output;
iree_host_size_t remaining = output_capacity;
while (state->emit_position < state->sequence_count && remaining > 0) {
uint32_t codepoint = state->sequence[state->emit_position];
iree_host_size_t written =
iree_tokenizer_nfd_emit_codepoint(state, codepoint, out_ptr, remaining);
out_ptr += written;
remaining -= written;
++state->emit_position;
// If we have pending_utf8 that couldn't be fully written, stop.
if (state->pending_utf8_count > state->pending_utf8_position) break;
}
// If fully emitted, reset the sequence.
if (state->emit_position >= state->sequence_count &&
state->pending_utf8_count == 0) {
state->sequence_count = 0;
state->emit_position = 0;
}
return (iree_host_size_t)(out_ptr - output);
}
// Orders the current sequence and begins emitting.
static iree_host_size_t iree_tokenizer_nfd_flush_and_emit(
iree_tokenizer_normalizer_nfd_state_t* state, uint8_t* output,
iree_host_size_t output_capacity) {
if (state->sequence_count == 0) return 0;
iree_tokenizer_nfd_order_sequence(state);
state->emit_position = 0;
return iree_tokenizer_nfd_emit_sequence(state, output, output_capacity);
}
// Drains all pending output.
static iree_host_size_t iree_tokenizer_nfd_drain_pending(
iree_tokenizer_normalizer_nfd_state_t* state, uint8_t* output,
iree_host_size_t output_capacity) {
uint8_t* out_ptr = output;
iree_host_size_t remaining = output_capacity;
// Drain partial UTF-8 from a previous emit.
if (state->pending_utf8_count > state->pending_utf8_position) {
iree_host_size_t written =
iree_tokenizer_nfd_emit_pending_utf8(state, out_ptr, remaining);
out_ptr += written;
remaining -= written;
if (state->pending_utf8_count > state->pending_utf8_position) {
return (iree_host_size_t)(out_ptr - output);
}
}
// Reset sequence if all codepoints were already emitted.
if (state->emit_position > 0 &&
state->emit_position >= state->sequence_count) {
state->sequence_count = 0;
state->emit_position = 0;
}
// Continue emitting a partially-emitted sequence.
if (state->emit_position > 0 &&
state->emit_position < state->sequence_count) {
iree_host_size_t written =
iree_tokenizer_nfd_emit_sequence(state, out_ptr, remaining);
out_ptr += written;
}
return (iree_host_size_t)(out_ptr - output);
}
//===----------------------------------------------------------------------===//
// Codepoint Ingestion
//===----------------------------------------------------------------------===//
// Ingests a single decomposed codepoint into the combining sequence.
//
// For starters (CCC=0): flushes and emits the previous sequence (if any),
// then starts a new sequence.
//
// For combining marks (CCC>0): appends to the current sequence.
static iree_status_t iree_tokenizer_nfd_ingest_codepoint(
iree_tokenizer_normalizer_nfd_state_t* state, uint32_t codepoint,
uint8_t** out_ptr, uint8_t* out_end, bool* out_consumed) {
*out_consumed = false;
uint8_t ccc = iree_unicode_ccc(codepoint);
iree_host_size_t remaining = (iree_host_size_t)(out_end - *out_ptr);
if (ccc == 0) {
// Starter character.
if (state->sequence_count == 0) {
// No pending sequence - just start a new one.
state->sequence[0] = codepoint;
state->sequence_count = 1;
*out_consumed = true;
} else {
// Previous sequence pending - flush it first.
iree_host_size_t written =
iree_tokenizer_nfd_flush_and_emit(state, *out_ptr, remaining);
*out_ptr += written;
if (state->sequence_count > 0) {
// Couldn't fully flush - can't consume this codepoint yet.
return iree_ok_status();
}
state->sequence[0] = codepoint;
state->sequence_count = 1;
*out_consumed = true;
}
} else {
// Combining mark.
if (state->sequence_count == 0) {
// Defective combining character sequence (no preceding starter).
state->sequence[0] = codepoint;
state->sequence_count = 1;
*out_consumed = true;
} else {
if (state->sequence_count >= IREE_TOKENIZER_NFD_MAX_SEQUENCE) {
return iree_make_status(
IREE_STATUS_RESOURCE_EXHAUSTED,
"NFD combining sequence exceeds Unicode Stream-Safe "
"limit of %d codepoints",
IREE_TOKENIZER_NFD_MAX_SEQUENCE);
}
state->sequence[state->sequence_count++] = codepoint;
*out_consumed = true;
}
}
return iree_ok_status();
}
//===----------------------------------------------------------------------===//
// Normalizer Vtable Implementation
//===----------------------------------------------------------------------===//
static iree_status_t iree_tokenizer_normalizer_nfd_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) {
iree_tokenizer_normalizer_nfd_state_t* state =
(iree_tokenizer_normalizer_nfd_state_t*)base_state;
// When SEGMENT_END is set, we flush pending combining sequence after
// processing input.
const bool is_segment_end =
iree_any_bit_set(flags, IREE_TOKENIZER_NORMALIZER_FLAG_SEGMENT_END);
const uint8_t* in_ptr = (const uint8_t*)input.data;
const uint8_t* in_end = in_ptr + input.size;
uint8_t* out_ptr = (uint8_t*)output.data;
uint8_t* out_end = out_ptr + output.size;
// Drain any pending output from a previous partial emit.
iree_host_size_t drained = iree_tokenizer_nfd_drain_pending(
state, out_ptr, (iree_host_size_t)(out_end - out_ptr));
out_ptr += drained;
if (state->pending_utf8_count > state->pending_utf8_position ||
(state->emit_position > 0 &&
state->emit_position < state->sequence_count)) {
// Still have pending output - can't process new input.
*out_consumed = 0;
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
// Main processing loop.
bool drain_stalled = false;
while (in_ptr < in_end ||
state->decomposed_position < state->decomposed_count) {
// First, drain any buffered decomposed codepoints from a previous call.
// This is needed for Hangul and other cases where one input codepoint
// decomposes to multiple output codepoints (potentially multiple starters).
while (state->decomposed_position < state->decomposed_count) {
bool consumed = false;
iree_status_t status = iree_tokenizer_nfd_ingest_codepoint(
state, state->decomposed_buffer[state->decomposed_position], &out_ptr,
out_end, &consumed);
if (!iree_status_is_ok(status)) {
*out_consumed = (iree_host_size_t)(in_ptr - (const uint8_t*)input.data);
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return status;
}
if (!consumed) {
drain_stalled = true;
break;
}
++state->decomposed_position;
}
if (drain_stalled) break;
// Clear the buffer once fully consumed.
state->decomposed_count = 0;
state->decomposed_position = 0;
// No more input to process.
if (in_ptr >= in_end) break;
// ASCII fast path: when no combining sequence is pending, emit ASCII bytes
// directly. We buffer the last ASCII byte before a non-ASCII byte (or end
// of input) because ASCII characters can have following combining marks
// (e.g., 'a' + U+0301 combining acute).
if (state->sequence_count == 0) {
while (in_ptr < in_end && out_ptr < out_end && (*in_ptr & 0x80) == 0) {
if (in_ptr + 1 >= in_end || (*(in_ptr + 1) & 0x80) != 0) {
// Last ASCII or next is non-ASCII - buffer for potential combining.
state->sequence[0] = *in_ptr++;
state->sequence_count = 1;
break;
}
*out_ptr++ = *in_ptr++;
}
if (in_ptr >= in_end ||
(state->sequence_count == 0 && out_ptr >= out_end))
break;
if (state->sequence_count > 0 && in_ptr >= in_end) break;
}
// Output full - stop processing.
if (out_ptr >= out_end &&
(state->sequence_count > 0 ||
state->pending_utf8_count > state->pending_utf8_position)) {
break;
}
// Decode the next codepoint.
iree_host_size_t byte_count;
if ((*in_ptr & 0x80) == 0) {
byte_count = 1;
} else {
byte_count = iree_unicode_utf8_sequence_length(*in_ptr);
}
iree_host_size_t available = (iree_host_size_t)(in_end - in_ptr);
IREE_ASSERT(available >= byte_count,
"incomplete UTF-8 at end of normalizer input violates "
"interface contract");
iree_string_view_t remaining_input = {
(const char*)in_ptr,
available,
};
iree_host_size_t decode_position = 0;
uint32_t codepoint =
iree_unicode_utf8_decode(remaining_input, &decode_position);
// NFD canonical decomposition: full decomposition to base + combining.
// Unlike NFC which uses decompose_nfc_canonical (preserves precomposed),
// NFD uses the full decompose function.
// Buffer the decomposed codepoints so we can resume if output is full.
state->decomposed_count =
(uint8_t)iree_unicode_decompose(codepoint, state->decomposed_buffer);
state->decomposed_position = 0;
// Advance input pointer - we've committed to processing this codepoint.
// The decomposed buffer will be drained on subsequent calls if needed.
in_ptr += byte_count;
}
// At segment end, flush pending combining sequence.
if (is_segment_end && state->sequence_count > 0 &&
state->emit_position == 0) {
iree_host_size_t written = iree_tokenizer_nfd_flush_and_emit(
state, out_ptr, (iree_host_size_t)(out_end - out_ptr));
out_ptr += written;
}
*out_consumed = (iree_host_size_t)(in_ptr - (const uint8_t*)input.data);
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
static iree_status_t iree_tokenizer_normalizer_nfd_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_nfd_state_t* state =
(iree_tokenizer_normalizer_nfd_state_t*)base_state;
uint8_t* out_ptr = (uint8_t*)output.data;
uint8_t* out_end = out_ptr + output.size;
// Drain any partially-emitted output.
iree_host_size_t drained = iree_tokenizer_nfd_drain_pending(
state, out_ptr, (iree_host_size_t)(out_end - out_ptr));
out_ptr += drained;
if (state->pending_utf8_count > state->pending_utf8_position ||
(state->emit_position > 0 &&
state->emit_position < state->sequence_count)) {
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
// Drain buffered decomposed codepoints (from Hangul or multi-codepoint
// decompositions that couldn't be fully ingested in process()).
while (state->decomposed_position < state->decomposed_count) {
bool consumed = false;
iree_status_t status = iree_tokenizer_nfd_ingest_codepoint(
state, state->decomposed_buffer[state->decomposed_position], &out_ptr,
out_end, &consumed);
if (!iree_status_is_ok(status)) {
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return status;
}
if (!consumed) {
// Output full - return what we have.
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
++state->decomposed_position;
}
state->decomposed_count = 0;
state->decomposed_position = 0;
// Order and emit the final combining sequence.
if (state->sequence_count > 0) {
iree_host_size_t written = iree_tokenizer_nfd_flush_and_emit(
state, out_ptr, (iree_host_size_t)(out_end - out_ptr));
out_ptr += written;
}
*out_written = (iree_host_size_t)(out_ptr - (uint8_t*)output.data);
return iree_ok_status();
}
static bool iree_tokenizer_normalizer_nfd_state_has_pending(
const iree_tokenizer_normalizer_state_t* base_state) {
const iree_tokenizer_normalizer_nfd_state_t* state =
(const iree_tokenizer_normalizer_nfd_state_t*)base_state;
return state->sequence_count > 0 ||
state->pending_utf8_count > state->pending_utf8_position ||
state->decomposed_position < state->decomposed_count;
}
static const iree_tokenizer_normalizer_vtable_t
iree_tokenizer_normalizer_nfd_vtable = {
.destroy = iree_tokenizer_normalizer_nfd_destroy,
.state_initialize = iree_tokenizer_normalizer_nfd_state_initialize,
.state_deinitialize = iree_tokenizer_normalizer_nfd_state_deinitialize,
.state_process = iree_tokenizer_normalizer_nfd_state_process,
.state_finalize = iree_tokenizer_normalizer_nfd_state_finalize,
.state_has_pending = iree_tokenizer_normalizer_nfd_state_has_pending,
};