blob: b7d162bb77e5c1b16f8ffd9e32f2e3536c7218c3 [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/postprocessor.h"
#include "iree/base/api.h"
#include "iree/tokenizer/vocab/vocab.h"
//===----------------------------------------------------------------------===//
// Validation
//===----------------------------------------------------------------------===//
static iree_status_t iree_tokenizer_postprocessor_validate_template(
const iree_tokenizer_postprocessor_template_t* t,
const char* template_name) {
uint8_t total = iree_tokenizer_postprocessor_template_total_count(t);
if (total > IREE_TOKENIZER_POSTPROCESSOR_MAX_PIECES) {
return iree_make_status(
IREE_STATUS_INVALID_ARGUMENT,
"postprocessor %s template has %u total pieces (max %u)", template_name,
(unsigned)total, (unsigned)IREE_TOKENIZER_POSTPROCESSOR_MAX_PIECES);
}
return iree_ok_status();
}
//===----------------------------------------------------------------------===//
// Lifecycle
//===----------------------------------------------------------------------===//
iree_status_t iree_tokenizer_postprocessor_initialize(
const iree_tokenizer_postprocessor_template_t* single_template,
const iree_tokenizer_postprocessor_template_t* pair_template,
iree_tokenizer_postprocessor_flags_t flags,
iree_tokenizer_postprocessor_t* out_postprocessor) {
IREE_ASSERT_ARGUMENT(single_template);
IREE_ASSERT_ARGUMENT(out_postprocessor);
IREE_TRACE_ZONE_BEGIN(z0);
memset(out_postprocessor, 0, sizeof(*out_postprocessor));
// Validate template sizes.
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, iree_tokenizer_postprocessor_validate_template(single_template,
"single"));
if (pair_template) {
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0,
iree_tokenizer_postprocessor_validate_template(pair_template, "pair"));
}
// Copy templates (fixed-size inline structs).
memcpy(&out_postprocessor->single, single_template,
sizeof(iree_tokenizer_postprocessor_template_t));
if (pair_template) {
memcpy(&out_postprocessor->pair, pair_template,
sizeof(iree_tokenizer_postprocessor_template_t));
out_postprocessor->has_pair = true;
}
out_postprocessor->flags = flags;
IREE_TRACE_ZONE_END(z0);
return iree_ok_status();
}
void iree_tokenizer_postprocessor_deinitialize(
iree_tokenizer_postprocessor_t* postprocessor) {
if (!postprocessor) return;
IREE_TRACE_ZONE_BEGIN(z0);
memset(postprocessor, 0, sizeof(*postprocessor));
IREE_TRACE_ZONE_END(z0);
}
//===----------------------------------------------------------------------===//
// Encode Operations
//===----------------------------------------------------------------------===//
// Emits special tokens from a template phase into the token output.
// Writes tokens starting at |output_offset|, bounded by output.capacity.
// |base| selects the starting index in the template's arrays.
// |position| tracks progress (in/out) and reaches |count| when complete.
// Special tokens get zero-length offsets (no corresponding source text).
static iree_host_size_t iree_tokenizer_postprocessor_emit_phase(
const iree_tokenizer_postprocessor_template_t* template_data, uint8_t base,
uint8_t count, uint8_t* IREE_RESTRICT position,
iree_tokenizer_token_output_t output, iree_host_size_t output_offset) {
iree_host_size_t emitted = 0;
while (*position < count) {
// Check for available capacity with overflow protection.
iree_host_size_t out_index = 0;
if (!iree_host_size_checked_add(output_offset, emitted, &out_index) ||
out_index >= output.capacity) {
break; // Overflow or no room.
}
uint8_t template_index = base + *position;
output.token_ids[out_index] = template_data->token_ids[template_index];
if (output.type_ids) {
output.type_ids[out_index] = template_data->type_ids[template_index];
}
if (output.token_offsets) {
output.token_offsets[out_index].start = 0;
output.token_offsets[out_index].end = 0;
}
++(*position);
++emitted;
}
return emitted;
}
iree_host_size_t iree_tokenizer_postprocessor_emit_prefix(
iree_tokenizer_postprocessor_encode_state_t* state,
iree_tokenizer_token_output_t output, iree_host_size_t output_offset) {
if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_PREFIX) return 0;
iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase(
state->active_template, /*base=*/0, state->active_template->prefix_count,
&state->position, output, output_offset);
if (state->position >= state->active_template->prefix_count) {
state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_A;
state->position = 0;
}
return emitted;
}
iree_host_size_t iree_tokenizer_postprocessor_emit_infix(
iree_tokenizer_postprocessor_encode_state_t* state,
iree_tokenizer_token_output_t output, iree_host_size_t output_offset) {
if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_INFIX) return 0;
uint8_t base = state->active_template->prefix_count;
iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase(
state->active_template, base, state->active_template->infix_count,
&state->position, output, output_offset);
if (state->position >= state->active_template->infix_count) {
state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_B;
state->position = 0;
}
return emitted;
}
void iree_tokenizer_postprocessor_assign_type_ids(
const iree_tokenizer_postprocessor_encode_state_t* state,
iree_tokenizer_token_output_t output, iree_host_size_t offset,
iree_host_size_t count) {
if (!output.type_ids || count == 0) return;
uint8_t type_id = 0;
if (state->phase == IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_A) {
type_id = state->active_template->sequence_a_type_id;
} else if (state->phase == IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_B) {
type_id = state->active_template->sequence_b_type_id;
} else {
return;
}
// Bounds check with overflow protection: offset + count must not exceed
// capacity and must not overflow.
iree_host_size_t end = 0;
if (!iree_host_size_checked_add(offset, count, &end) ||
end > output.capacity) {
return; // Overflow or out of bounds.
}
memset(&output.type_ids[offset], type_id, count);
}
iree_host_size_t iree_tokenizer_postprocessor_emit_suffix(
iree_tokenizer_postprocessor_encode_state_t* state,
iree_tokenizer_token_output_t output, iree_host_size_t output_offset) {
if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_SUFFIX) return 0;
uint8_t base = state->active_template->prefix_count +
state->active_template->infix_count;
iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase(
state->active_template, base, state->active_template->suffix_count,
&state->position, output, output_offset);
if (state->position >= state->active_template->suffix_count) {
state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_DONE;
}
return emitted;
}
//===----------------------------------------------------------------------===//
// Offset Trimming
//===----------------------------------------------------------------------===//
// Counts leading whitespace in a token string, returning the number of bytes
// in the original input that should be trimmed from offset.start.
//
// ByteLevel encoding uses special Unicode characters to represent whitespace:
// U+0120 (Ġ, UTF-8: 0xC4 0xA0) represents space (0x20)
// U+010A (Ċ, UTF-8: 0xC4 0x8A) represents newline (0x0A)
// U+010D (ċ, UTF-8: 0xC4 0x8D) represents carriage return (0x0D)
// Each sentinel in the token text corresponds to exactly 1 byte in the
// original input.
//
// Returns the count in terms of ORIGINAL INPUT bytes, not token text bytes.
static iree_host_size_t iree_tokenizer_postprocessor_count_leading_whitespace(
iree_string_view_t text) {
iree_host_size_t count = 0;
iree_host_size_t position = 0;
while (position < text.size) {
uint8_t byte = (uint8_t)text.data[position];
if (byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D) {
// ASCII whitespace: space, tab, newline, carriage return.
// 1 byte in token text = 1 byte in original input.
++count;
++position;
} else if (position + 1 < text.size && byte == 0xC4) {
// Check for ByteLevel whitespace sentinels (all start with 0xC4).
uint8_t next_byte = (uint8_t)text.data[position + 1];
if (next_byte == 0xA0 || next_byte == 0x8A || next_byte == 0x8D) {
// Ġ (0xA0) = space, Ċ (0x8A) = newline, ċ (0x8D) = carriage return.
// 2 bytes in token text = 1 byte in original input.
++count;
position += 2;
} else {
break;
}
} else {
// Non-whitespace character.
break;
}
}
return count;
}
// Counts trailing whitespace in a token string, returning the number of bytes
// in the original input that should be trimmed from offset.end.
static iree_host_size_t iree_tokenizer_postprocessor_count_trailing_whitespace(
iree_string_view_t text) {
iree_host_size_t count = 0;
iree_host_size_t position = text.size;
while (position > 0) {
uint8_t byte = (uint8_t)text.data[position - 1];
if (byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D) {
// ASCII whitespace.
++count;
--position;
} else if (position >= 2 && (uint8_t)text.data[position - 2] == 0xC4) {
// Check for ByteLevel whitespace sentinels (second byte of UTF-8).
if (byte == 0xA0 || byte == 0x8A || byte == 0x8D) {
// Ġ (0xA0) = space, Ċ (0x8A) = newline, ċ (0x8D) = carriage return.
++count;
position -= 2;
} else {
break;
}
} else {
break;
}
}
return count;
}
void iree_tokenizer_postprocessor_trim_token_offsets(
iree_tokenizer_postprocessor_encode_state_t* state,
const iree_tokenizer_vocab_t* vocab, iree_tokenizer_token_output_t output,
iree_host_size_t model_token_start, iree_host_size_t model_token_count) {
// Early exit if trimming is disabled or no offsets to trim.
if (!state ||
!iree_any_bit_set(state->flags,
IREE_TOKENIZER_POSTPROCESSOR_FLAG_TRIM_OFFSETS)) {
return;
}
if (!output.token_offsets || model_token_count == 0) return;
if (!vocab) return;
for (iree_host_size_t i = 0; i < model_token_count; ++i) {
// Compute token_index with overflow protection.
iree_host_size_t token_index = 0;
if (!iree_host_size_checked_add(model_token_start, i, &token_index) ||
token_index >= output.capacity) {
break;
}
iree_tokenizer_token_id_t token_id = output.token_ids[token_index];
iree_string_view_t token_text =
iree_tokenizer_vocab_token_text(vocab, token_id);
if (token_text.size == 0) continue;
iree_host_size_t leading =
iree_tokenizer_postprocessor_count_leading_whitespace(token_text);
iree_host_size_t trailing =
iree_tokenizer_postprocessor_count_trailing_whitespace(token_text);
// Special case: if ADD_PREFIX_SPACE is set and this is the first model
// token across the entire encode, preserve exactly 1 leading space (it was
// artificially added and is part of the token's content).
if (!state->first_model_token_trimmed &&
iree_any_bit_set(state->flags,
IREE_TOKENIZER_POSTPROCESSOR_FLAG_ADD_PREFIX_SPACE) &&
leading > 0) {
--leading;
}
state->first_model_token_trimmed = true;
iree_tokenizer_offset_t* offset = &output.token_offsets[token_index];
// Trim leading: advance start but don't exceed end. Use checked arithmetic
// to avoid overflow when offset->start + leading exceeds SIZE_MAX.
iree_host_size_t new_start = 0;
if (!iree_host_size_checked_add(offset->start, leading, &new_start) ||
new_start > offset->end) {
new_start = offset->end;
}
// Trim trailing: move end back but don't go below new_start.
// The span available for trimming is [new_start, offset->end].
iree_host_size_t new_end = offset->end;
if (trailing > 0) {
iree_host_size_t span = new_end - new_start;
if (trailing >= span) {
new_end = new_start;
} else {
new_end -= trailing;
}
}
offset->start = new_start;
offset->end = new_end;
}
}