| // 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; |
| } |
| } |