Foundry/tokenizer/tokenizer.c

951 lines
27 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
#include "tokenizer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define FT_MAX_VOCAB 100000
#define FT_MAX_MERGES 100000
#define FT_MAX_TOKEN_LEN 256
#define FT_HASH_SIZE 65536
/* ============================================================= */
/* Hash Table for Token Lookup */
/* ============================================================= */
typedef struct ft_hash_entry {
char* key;
int value;
struct ft_hash_entry* next;
} ft_hash_entry;
typedef struct {
ft_hash_entry* buckets[FT_HASH_SIZE];
} ft_hash_table;
static uint32_t hash_string(const char* str) {
uint32_t hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash % FT_HASH_SIZE;
}
static ft_hash_table* hash_new(void) {
ft_hash_table* ht = calloc(1, sizeof(ft_hash_table));
return ht;
}
static void hash_free(ft_hash_table* ht) {
if (!ht) return;
for (int i = 0; i < FT_HASH_SIZE; i++) {
ft_hash_entry* e = ht->buckets[i];
while (e) {
ft_hash_entry* next = e->next;
free(e->key);
free(e);
e = next;
}
}
free(ht);
}
static void hash_set(ft_hash_table* ht, const char* key, int value) {
uint32_t idx = hash_string(key);
ft_hash_entry* e = ht->buckets[idx];
while (e) {
if (strcmp(e->key, key) == 0) {
e->value = value;
return;
}
e = e->next;
}
e = malloc(sizeof(ft_hash_entry));
e->key = strdup(key);
e->value = value;
e->next = ht->buckets[idx];
ht->buckets[idx] = e;
}
static int hash_get(ft_hash_table* ht, const char* key, int default_val) {
uint32_t idx = hash_string(key);
ft_hash_entry* e = ht->buckets[idx];
while (e) {
if (strcmp(e->key, key) == 0) {
return e->value;
}
e = e->next;
}
return default_val;
}
/* ============================================================= */
/* BPE Merge Rule */
/* ============================================================= */
typedef struct {
char* a;
char* b;
char* merged; /* a + b concatenated */
} ft_merge;
/* ============================================================= */
/* Tokenizer Structure */
/* ============================================================= */
struct ft_tokenizer {
/* Vocabulary */
char** tokens; /* id -> token string */
int vocab_size;
int vocab_capacity;
ft_hash_table* token_to_id;
/* BPE merges (in priority order) */
ft_merge* merges;
int num_merges;
int merge_capacity;
ft_hash_table* merge_lookup; /* "a b" -> merge index */
/* Special tokens */
int unk_id;
int bos_id;
int eos_id;
int pad_id;
/* SentencePiece / Unigram fields */
int model_type; /* 0=BPE (default), 1=UNIGRAM */
float *scores; /* [vocab_size] log-prob per token, NULL for BPE */
int max_token_len; /* longest token in bytes (for Viterbi scan) */
/* BPE pre-tokenizer family (tokenizer.ggml.pre); consumer-defined code,
* 0 = default/unset. Selects the regex pre-tokenization scanner. */
int pre_type;
};
/* ============================================================= */
/* Creation / Destruction */
/* ============================================================= */
ft_tokenizer* ft_new(void) {
ft_tokenizer* tok = calloc(1, sizeof(ft_tokenizer));
if (!tok) return NULL;
tok->vocab_capacity = 1024;
tok->tokens = calloc(tok->vocab_capacity, sizeof(char*));
tok->token_to_id = hash_new();
tok->merge_capacity = 1024;
tok->merges = calloc(tok->merge_capacity, sizeof(ft_merge));
tok->merge_lookup = hash_new();
tok->unk_id = -1;
tok->bos_id = -1;
tok->eos_id = -1;
tok->pad_id = -1;
return tok;
}
void ft_free(ft_tokenizer* tok) {
if (!tok) return;
for (int i = 0; i < tok->vocab_size; i++) {
free(tok->tokens[i]);
}
free(tok->tokens);
hash_free(tok->token_to_id);
for (int i = 0; i < tok->num_merges; i++) {
free(tok->merges[i].a);
free(tok->merges[i].b);
free(tok->merges[i].merged);
}
free(tok->merges);
hash_free(tok->merge_lookup);
free(tok->scores);
free(tok);
}
/* ============================================================= */
/* Vocabulary Management */
/* ============================================================= */
int ft_add_token(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return -1;
/* Check if already exists */
int existing = hash_get(tok->token_to_id, token, -1);
if (existing >= 0) return existing;
/* Expand if needed */
if (tok->vocab_size >= tok->vocab_capacity) {
tok->vocab_capacity *= 2;
tok->tokens = realloc(tok->tokens, tok->vocab_capacity * sizeof(char*));
}
int id = tok->vocab_size;
tok->tokens[id] = strdup(token);
hash_set(tok->token_to_id, token, id);
tok->vocab_size++;
return id;
}
void ft_add_merge(ft_tokenizer* tok, const char* a, const char* b) {
if (!tok || !a || !b) return;
/* Expand if needed */
if (tok->num_merges >= tok->merge_capacity) {
tok->merge_capacity *= 2;
tok->merges = realloc(tok->merges, tok->merge_capacity * sizeof(ft_merge));
}
ft_merge* m = &tok->merges[tok->num_merges];
m->a = strdup(a);
m->b = strdup(b);
/* Create merged token */
size_t len_a = strlen(a);
size_t len_b = strlen(b);
m->merged = malloc(len_a + len_b + 1);
memcpy(m->merged, a, len_a);
memcpy(m->merged + len_a, b, len_b);
m->merged[len_a + len_b] = '\0';
/* Add to lookup */
char key[FT_MAX_TOKEN_LEN * 2 + 2];
snprintf(key, sizeof(key), "%s %s", a, b);
hash_set(tok->merge_lookup, key, tok->num_merges);
tok->num_merges++;
}
int ft_vocab_size(ft_tokenizer* tok) {
return tok ? tok->vocab_size : 0;
}
const char* ft_id_to_token(ft_tokenizer* tok, int id) {
if (!tok || id < 0 || id >= tok->vocab_size) return NULL;
return tok->tokens[id];
}
int ft_token_to_id(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return -1;
int id = hash_get(tok->token_to_id, token, -1);
/* Return unk_id for unknown tokens, but only if unk is set */
if (id < 0 && tok->unk_id >= 0) {
return tok->unk_id;
}
return id;
}
/* ============================================================= */
/* Special Tokens */
/* ============================================================= */
/* Helper: check if token exists in vocab (without unk fallback) */
static int token_exists(ft_tokenizer* tok, const char* token) {
return hash_get(tok->token_to_id, token, -1);
}
void ft_set_unk_token(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return;
tok->unk_id = token_exists(tok, token);
if (tok->unk_id < 0) tok->unk_id = ft_add_token(tok, token);
}
void ft_set_bos_token(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return;
tok->bos_id = token_exists(tok, token);
if (tok->bos_id < 0) tok->bos_id = ft_add_token(tok, token);
}
void ft_set_eos_token(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return;
tok->eos_id = token_exists(tok, token);
if (tok->eos_id < 0) tok->eos_id = ft_add_token(tok, token);
}
void ft_set_pad_token(ft_tokenizer* tok, const char* token) {
if (!tok || !token) return;
tok->pad_id = token_exists(tok, token);
if (tok->pad_id < 0) tok->pad_id = ft_add_token(tok, token);
}
int ft_unk_id(ft_tokenizer* tok) { return tok ? tok->unk_id : -1; }
int ft_bos_id(ft_tokenizer* tok) { return tok ? tok->bos_id : -1; }
int ft_eos_id(ft_tokenizer* tok) { return tok ? tok->eos_id : -1; }
int ft_pad_id(ft_tokenizer* tok) { return tok ? tok->pad_id : -1; }
/* ============================================================= */
/* Model Type (BPE vs Unigram) */
/* ============================================================= */
void ft_set_model_type(ft_tokenizer *tok, int type) {
if (tok) tok->model_type = type;
}
int ft_get_model_type(ft_tokenizer *tok) {
return tok ? tok->model_type : 0;
}
void ft_set_pre_type(ft_tokenizer *tok, int type) {
if (tok) tok->pre_type = type;
}
int ft_get_pre_type(ft_tokenizer *tok) {
return tok ? tok->pre_type : 0;
}
void ft_set_scores(ft_tokenizer *tok, const float *scores, int n) {
if (!tok || !scores || n <= 0) return;
free(tok->scores);
tok->scores = malloc(n * sizeof(float));
if (!tok->scores) return;
memcpy(tok->scores, scores, n * sizeof(float));
/* Compute max_token_len by scanning vocab */
tok->max_token_len = 0;
for (int i = 0; i < tok->vocab_size; i++) {
if (tok->tokens[i]) {
int len = (int)strlen(tok->tokens[i]);
if (len > tok->max_token_len) tok->max_token_len = len;
}
}
/* Cap to something reasonable */
if (tok->max_token_len > 64) tok->max_token_len = 64;
}
/* ============================================================= */
/* Unigram (SentencePiece) Encoding — Viterbi */
/* ============================================================= */
static int* encode_unigram(ft_tokenizer *tok, const char *text, int *length) {
int text_len = (int)strlen(text);
if (text_len == 0) {
*length = 0;
return calloc(1, sizeof(int));
}
int max_len = tok->max_token_len > 0 ? tok->max_token_len : 32;
/* Viterbi DP arrays */
float *best_score = malloc((text_len + 1) * sizeof(float));
int *best_token = malloc((text_len + 1) * sizeof(int));
int *best_from = malloc((text_len + 1) * sizeof(int));
best_score[0] = 0.0f;
for (int i = 1; i <= text_len; i++) {
best_score[i] = -1e30f;
best_token[i] = -1;
best_from[i] = -1;
}
/* Temp buffer for substring lookups */
char substr[256];
/* Forward pass */
for (int i = 0; i < text_len; i++) {
if (best_score[i] <= -1e30f) continue;
int remaining = text_len - i;
int scan_len = remaining < max_len ? remaining : max_len;
for (int len = 1; len <= scan_len; len++) {
if (len >= (int)sizeof(substr)) break;
memcpy(substr, text + i, len);
substr[len] = '\0';
/* Look up in hash table (without unk fallback) */
int id = hash_get(tok->token_to_id, substr, -1);
if (id < 0) continue;
float score = tok->scores ? tok->scores[id] : 0.0f;
float candidate = best_score[i] + score;
if (candidate > best_score[i + len]) {
best_score[i + len] = candidate;
best_token[i + len] = id;
best_from[i + len] = i;
}
}
/* Byte fallback: if no token starts here, emit <0xXX> */
if (best_score[i + 1] <= -1e30f) {
unsigned char byte = (unsigned char)text[i];
char hex_token[7];
snprintf(hex_token, sizeof(hex_token), "<0x%02X>", byte);
int id = hash_get(tok->token_to_id, hex_token, -1);
if (id >= 0) {
float score = tok->scores ? tok->scores[id] : -100.0f;
best_score[i + 1] = best_score[i] + score;
best_token[i + 1] = id;
best_from[i + 1] = i;
} else {
/* Last resort: unk token */
best_score[i + 1] = best_score[i] - 100.0f;
best_token[i + 1] = tok->unk_id >= 0 ? tok->unk_id : 0;
best_from[i + 1] = i;
}
}
}
/* Backtrack to collect token sequence */
int *rev_tokens = malloc(text_len * sizeof(int));
int n = 0;
int pos = text_len;
while (pos > 0) {
rev_tokens[n++] = best_token[pos];
pos = best_from[pos];
}
free(best_score);
free(best_token);
free(best_from);
/* Reverse into output */
int *ids = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
ids[i] = rev_tokens[n - 1 - i];
}
free(rev_tokens);
*length = n;
return ids;
}
/* ============================================================= */
/* BPE Encoding */
/* ============================================================= */
/* Split text into initial character-level tokens */
static char** split_to_chars(const char* text, int* count) {
int len = (int)strlen(text);
char** chars = malloc(len * sizeof(char*));
int n = 0;
const unsigned char* p = (const unsigned char*)text;
while (*p) {
int char_len = 1;
/* Handle UTF-8 */
if (*p >= 0xF0) char_len = 4;
else if (*p >= 0xE0) char_len = 3;
else if (*p >= 0xC0) char_len = 2;
chars[n] = malloc(char_len + 1);
memcpy(chars[n], p, char_len);
chars[n][char_len] = '\0';
n++;
p += char_len;
}
*count = n;
return chars;
}
/* Apply BPE merges to token list */
static void apply_bpe_merges(ft_tokenizer* tok, char** tokens, int* count) {
bool changed = true;
while (changed) {
changed = false;
int best_merge = -1;
int best_pos = -1;
/* Find highest priority merge that can be applied */
for (int i = 0; i < *count - 1; i++) {
char key[FT_MAX_TOKEN_LEN * 2 + 2];
snprintf(key, sizeof(key), "%s %s", tokens[i], tokens[i + 1]);
int merge_idx = hash_get(tok->merge_lookup, key, -1);
if (merge_idx >= 0 && (best_merge < 0 || merge_idx < best_merge)) {
best_merge = merge_idx;
best_pos = i;
}
}
/* Apply the merge */
if (best_merge >= 0) {
ft_merge* m = &tok->merges[best_merge];
/* Replace tokens[best_pos] and tokens[best_pos+1] with merged */
free(tokens[best_pos]);
free(tokens[best_pos + 1]);
tokens[best_pos] = strdup(m->merged);
/* Shift remaining tokens */
for (int i = best_pos + 1; i < *count - 1; i++) {
tokens[i] = tokens[i + 1];
}
(*count)--;
changed = true;
}
}
}
int* ft_encode(ft_tokenizer* tok, const char* text, int* length) {
if (!tok || !text || !length) return NULL;
if (strlen(text) == 0) {
*length = 0;
return calloc(1, sizeof(int));
}
/* Unigram / SentencePiece path */
if (tok->model_type == 1) {
/* SentencePiece expects a leading space for word boundary normalization */
size_t len = strlen(text);
char *spaced = malloc(len + 2);
spaced[0] = ' ';
memcpy(spaced + 1, text, len + 1);
int *ids = encode_unigram(tok, spaced, length);
free(spaced);
return ids;
}
/* Split into characters */
int char_count;
char** chars = split_to_chars(text, &char_count);
if (char_count == 0) {
free(chars);
*length = 0;
return calloc(1, sizeof(int));
}
/* Apply BPE merges */
if (tok->num_merges > 0) {
apply_bpe_merges(tok, chars, &char_count);
}
/* Convert to token IDs */
int* ids = malloc(char_count * sizeof(int));
for (int i = 0; i < char_count; i++) {
ids[i] = ft_token_to_id(tok, chars[i]);
free(chars[i]);
}
free(chars);
*length = char_count;
return ids;
}
int* ft_encode_with_special(ft_tokenizer* tok, const char* text, int* length,
bool add_bos, bool add_eos) {
int base_len;
int* base_ids = ft_encode(tok, text, &base_len);
if (!base_ids) return NULL;
int extra = (add_bos ? 1 : 0) + (add_eos ? 1 : 0);
int total_len = base_len + extra;
int* ids = malloc(total_len * sizeof(int));
int pos = 0;
if (add_bos && tok->bos_id >= 0) {
ids[pos++] = tok->bos_id;
}
memcpy(ids + pos, base_ids, base_len * sizeof(int));
pos += base_len;
if (add_eos && tok->eos_id >= 0) {
ids[pos++] = tok->eos_id;
}
free(base_ids);
*length = pos;
return ids;
}
int** ft_encode_batch(ft_tokenizer* tok, const char** texts, int n, int** lengths) {
if (!tok || !texts || !lengths || n <= 0) return NULL;
int** results = malloc(n * sizeof(int*));
*lengths = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
results[i] = ft_encode(tok, texts[i], &(*lengths)[i]);
}
return results;
}
/* ============================================================= */
/* Decoding */
/* ============================================================= */
/* Parse <0xHH> byte token, returns byte value or -1 */
static int parse_byte_token(const char *token) {
if (!token) return -1;
if (token[0] == '<' && token[1] == '0' && token[2] == 'x' &&
token[5] == '>' && token[6] == '\0') {
char hex[3] = { token[3], token[4], '\0' };
return (int)strtol(hex, NULL, 16);
}
return -1;
}
/* Append token to buffer, handling SentencePiece ▁ and <0xHH> byte tokens */
static size_t append_token_sp(const char *token, char *buf, size_t pos) {
const unsigned char *p = (const unsigned char *)token;
while (*p) {
/* Check for ▁ (U+2581 = 0xE2 0x96 0x81) — SentencePiece word separator */
if (p[0] == 0xE2 && p[1] == 0x96 && p[2] == 0x81) {
buf[pos++] = ' ';
p += 3;
} else {
buf[pos++] = (char)*p;
p++;
}
}
return pos;
}
char* ft_decode(ft_tokenizer* tok, int* ids, int length) {
if (!tok || !ids || length <= 0) {
char* empty = malloc(1);
empty[0] = '\0';
return empty;
}
/* BPE path: simple concatenation */
if (tok->model_type != 1) {
size_t total_len = 0;
for (int i = 0; i < length; i++) {
const char* token = ft_id_to_token(tok, ids[i]);
if (token) total_len += strlen(token);
}
char* result = malloc(total_len + 1);
result[0] = '\0';
for (int i = 0; i < length; i++) {
const char* token = ft_id_to_token(tok, ids[i]);
if (token) strcat(result, token);
}
return result;
}
/* Unigram/SentencePiece path: handle ▁ and <0xHH> byte tokens */
size_t total_len = 0;
for (int i = 0; i < length; i++) {
const char* token = ft_id_to_token(tok, ids[i]);
if (token) total_len += strlen(token);
}
char *result = malloc(total_len + 1);
size_t pos = 0;
for (int i = 0; i < length; i++) {
const char *token = ft_id_to_token(tok, ids[i]);
if (!token) continue;
int byte_val = parse_byte_token(token);
if (byte_val >= 0) {
result[pos++] = (char)byte_val;
} else {
pos = append_token_sp(token, result, pos);
}
}
result[pos] = '\0';
return result;
}
char* ft_decode_skip_special(ft_tokenizer* tok, int* ids, int length) {
if (!tok || !ids || length <= 0) {
char* empty = malloc(1);
empty[0] = '\0';
return empty;
}
/* Calculate total length, skipping special tokens */
size_t total_len = 0;
for (int i = 0; i < length; i++) {
if (ids[i] == tok->bos_id || ids[i] == tok->eos_id ||
ids[i] == tok->pad_id) continue;
const char* token = ft_id_to_token(tok, ids[i]);
if (token) total_len += strlen(token);
}
char* result = malloc(total_len + 1);
result[0] = '\0';
for (int i = 0; i < length; i++) {
if (ids[i] == tok->bos_id || ids[i] == tok->eos_id ||
ids[i] == tok->pad_id) continue;
const char* token = ft_id_to_token(tok, ids[i]);
if (token) strcat(result, token);
}
return result;
}
/* ============================================================= */
/* File Loading */
/* ============================================================= */
/* Simple JSON string extraction (not a full parser) */
static char* json_get_string(const char* json, const char* key) {
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char* pos = strstr(json, search);
if (!pos) return NULL;
pos += strlen(search);
while (*pos && (*pos == ' ' || *pos == ':' || *pos == '\t')) pos++;
if (*pos != '"') return NULL;
pos++;
const char* end = pos;
while (*end && *end != '"') {
if (*end == '\\') end++;
if (*end) end++;
}
size_t len = end - pos;
char* result = malloc(len + 1);
memcpy(result, pos, len);
result[len] = '\0';
return result;
}
static char* read_file(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char* content = malloc(size + 1);
size_t read = fread(content, 1, size, f);
content[read] = '\0';
fclose(f);
return content;
}
ft_tokenizer* ft_load_bpe(const char* vocab_path, const char* merges_path) {
ft_tokenizer* tok = ft_new();
if (!tok) return NULL;
/* Load vocab file (JSON format: {"token": id, ...}) */
char* vocab_content = read_file(vocab_path);
if (vocab_content) {
/* Parse simple JSON vocab */
const char* p = vocab_content;
while ((p = strchr(p, '"')) != NULL) {
p++;
const char* token_start = p;
while (*p && *p != '"') {
if (*p == '\\') p++;
if (*p) p++;
}
if (!*p) break;
size_t token_len = p - token_start;
char* token = malloc(token_len + 1);
memcpy(token, token_start, token_len);
token[token_len] = '\0';
p++; /* Skip closing quote */
/* Find colon and number */
while (*p && *p != ':') p++;
if (!*p) { free(token); break; }
p++;
while (*p && isspace(*p)) p++;
int id = (int)strtol(p, NULL, 10);
/* Ensure token is at correct position */
while (tok->vocab_size <= id) {
ft_add_token(tok, "");
}
free(tok->tokens[id]);
tok->tokens[id] = token;
hash_set(tok->token_to_id, token, id);
}
free(vocab_content);
}
/* Load merges file (one merge per line: "token1 token2") */
char* merges_content = read_file(merges_path);
if (merges_content) {
char* line = strtok(merges_content, "\n");
while (line) {
/* Skip comments and empty lines */
if (line[0] == '#' || line[0] == '\0') {
line = strtok(NULL, "\n");
continue;
}
/* Split on space */
char* space = strchr(line, ' ');
if (space) {
*space = '\0';
char* a = line;
char* b = space + 1;
/* Trim trailing whitespace */
char* end = b + strlen(b) - 1;
while (end > b && isspace(*end)) *end-- = '\0';
if (strlen(a) > 0 && strlen(b) > 0) {
ft_add_merge(tok, a, b);
/* Add merged token to vocab if not present */
char merged[FT_MAX_TOKEN_LEN];
snprintf(merged, sizeof(merged), "%s%s", a, b);
if (ft_token_to_id(tok, merged) < 0) {
ft_add_token(tok, merged);
}
}
}
line = strtok(NULL, "\n");
}
free(merges_content);
}
return tok;
}
ft_tokenizer* ft_load(const char* path) {
char* content = read_file(path);
if (!content) return NULL;
ft_tokenizer* tok = ft_new();
/* Try to parse as HuggingFace tokenizer.json format */
/* This is simplified - real implementation would need full JSON parser */
/* Look for vocab in "model" -> "vocab" */
const char* vocab_start = strstr(content, "\"vocab\"");
if (vocab_start) {
vocab_start = strchr(vocab_start, '{');
if (vocab_start) {
/* Parse vocab entries */
const char* p = vocab_start + 1;
while (*p && *p != '}') {
while (*p && *p != '"') p++;
if (!*p || *p == '}') break;
p++;
const char* token_start = p;
while (*p && *p != '"') {
if (*p == '\\') p++;
if (*p) p++;
}
size_t token_len = p - token_start;
char* token = malloc(token_len + 1);
memcpy(token, token_start, token_len);
token[token_len] = '\0';
/* Handle escape sequences */
/* (simplified - just handle common ones) */
p++;
while (*p && *p != ':') p++;
p++;
while (*p && isspace(*p)) p++;
int id = (int)strtol(p, (char**)&p, 10);
while (tok->vocab_size <= id) {
ft_add_token(tok, "");
}
free(tok->tokens[id]);
tok->tokens[id] = token;
hash_set(tok->token_to_id, token, id);
while (*p && *p != ',' && *p != '}') p++;
if (*p == ',') p++;
}
}
}
/* Look for special tokens */
char* unk = json_get_string(content, "unk_token");
char* bos = json_get_string(content, "bos_token");
char* eos = json_get_string(content, "eos_token");
char* pad = json_get_string(content, "pad_token");
if (unk) { ft_set_unk_token(tok, unk); free(unk); }
if (bos) { ft_set_bos_token(tok, bos); free(bos); }
if (eos) { ft_set_eos_token(tok, eos); free(eos); }
if (pad) { ft_set_pad_token(tok, pad); free(pad); }
free(content);
return tok;
}
/* ============================================================= */
/* Merge Rules */
/* ============================================================= */
int ft_num_merges(ft_tokenizer* tok) {
return tok ? tok->num_merges : 0;
}
void ft_get_merge(ft_tokenizer* tok, int index, const char **a, const char **b) {
if (!tok || index < 0 || index >= tok->num_merges) {
if (a) *a = NULL;
if (b) *b = NULL;
return;
}
if (a) *a = tok->merges[index].a;
if (b) *b = tok->merges[index].b;
}
/* ============================================================= */
/* Utility */
/* ============================================================= */
void ft_print(ft_tokenizer* tok) {
if (!tok) {
printf("tokenizer(NULL)\n");
return;
}
printf("tokenizer(vocab_size=%d, merges=%d)\n", tok->vocab_size, tok->num_merges);
printf(" special: unk=%d, bos=%d, eos=%d, pad=%d\n",
tok->unk_id, tok->bos_id, tok->eos_id, tok->pad_id);
}
void ft_print_tokens(ft_tokenizer* tok, int* ids, int length) {
if (!tok || !ids) return;
printf("[");
for (int i = 0; i < length; i++) {
const char* token = ft_id_to_token(tok, ids[i]);
printf("%d:'%s'%s", ids[i], token ? token : "?", i < length - 1 ? ", " : "");
}
printf("]\n");
}