// 词表加载与规范导出。
//
// 支持两种输入:
// 1. `.tiktoken` 风格:每行 ` `(rank 即 id)。
// —— 这是**数据格式**的处理,不是代码移植(计划 §5.5)。
// 2. 自有规范文本格式 `tally-vocab v1`(可 diff、可指纹,见 export.mbt)。
//
// 自有格式把 id 与 rank 显式分开,且 id 必须稠密升序,
// 这样导出是逐字节稳定的(不变量 V10)。
///|
/// 自有格式的版本标识行。
pub let format_header : String = "# tally-vocab v1"
///|
/// 把一行按空格切成非空字段。格式是规范的,不做多余的宽容。
fn split_fields(line : StringView) -> Array[String] {
let out : Array[String] = []
for p in line.split(" ") {
if p.length() > 0 {
out.push(p.to_owned())
}
}
out
}
///|
/// 解析非负十进制整数(不依赖标准库的整数解析,避免版本漂移)。
fn parse_dec(s : StringView) -> Int? {
let chars = s.to_array()
let n = chars.length()
if n == 0 {
return None
}
let mut v = 0
for i = 0; i < n; i = i + 1 {
let d = chars[i].to_int() - 0x30
if d < 0 || d > 9 {
return None
}
v = v * 10 + d
}
Some(v)
}
///|
/// 加载自有规范文本格式。
pub fn load(text : String) -> Vocab raise VocabError {
let tokens : Array[Bytes] = []
let ranks : Map[Bytes, Int] = Map([])
let specials : Array[(String, Int)] = []
let mut section = 0 // 0=未开始 1=specials 2=tokens
let mut line_no = 0
// 字节序列 -> 首次出现的行号。重复检测在解析阶段做,报错才带行号;
// build_vocab 里的同一不变量检查留作兜底。
let first_line : Map[Bytes, Int] = Map([])
for raw in text.split("\n") {
line_no = line_no + 1
let line = raw.trim()
if line.length() == 0 {
continue
}
if line.has_prefix("#") {
continue
}
if line == "[specials]" {
section = 1
continue
}
if line == "[tokens]" {
section = 2
continue
}
let parts = split_fields(line)
if section == 1 {
if parts.length() != 2 {
raise MalformedLine(line_no, line.to_owned())
}
match parse_dec(parts[0]) {
None => raise MalformedLine(line_no, line.to_owned())
Some(id) => {
let lit = b64_decode(parts[1])
specials.push((@utf8.decode_lossy(lit), id))
}
}
} else if section == 2 {
if parts.length() != 3 {
raise MalformedLine(line_no, line.to_owned())
}
match (parse_dec(parts[0]), parse_dec(parts[1])) {
(Some(id), Some(rank)) => {
if id != tokens.length() {
// id 必须稠密升序,否则导出不稳定
raise MalformedLine(line_no, line.to_owned())
}
let bytes = b64_decode(parts[2])
match first_line.get(bytes) {
Some(prev) => raise DuplicateToken(prev, line_no)
None => first_line.set(bytes, line_no)
}
tokens.push(bytes)
ranks.set(bytes, rank)
}
_ => raise MalformedLine(line_no, line.to_owned())
}
} else {
raise UnknownSection(line.to_owned())
}
}
build_vocab(tokens, ranks, specials)
}
///|
/// 加载 `.tiktoken` 风格的词表:每行 ` `,rank 即 id。
///
/// `specials` 由调用方传入(字面量, id),因为该格式不携带特殊 token 定义。
pub fn load_tiktoken(
text : String,
specials : Array[(String, Int)],
) -> Vocab raise VocabError {
let pairs : Array[(Bytes, Int)] = []
let mut max_rank = -1
let mut line_no = 0
let first_line : Map[Bytes, Int] = Map([])
// rank -> 行号。两个人抢同一个 rank 时,如果只保留后者,词表里就会
// 出现"ranks 有它、tokens 没有它"的错位——那不是词表,那是数据事故。
let rank_line : Map[Int, Int] = Map([])
for raw in text.split("\n") {
line_no = line_no + 1
let line = raw.trim()
if line.length() == 0 {
continue
}
let parts = split_fields(line)
if parts.length() != 2 {
raise MalformedLine(line_no, line.to_owned())
}
let bytes = b64_decode(parts[0])
match first_line.get(bytes) {
Some(prev) => raise DuplicateToken(prev, line_no)
None => first_line.set(bytes, line_no)
}
match parse_dec(parts[1]) {
None => raise MalformedLine(line_no, line.to_owned())
Some(rank) => {
match rank_line.get(rank) {
Some(prev) =>
raise MalformedLine(
line_no,
"duplicate rank " +
rank.to_string() +
" (already on line " +
prev.to_string() +
")",
)
None => rank_line.set(rank, line_no)
}
pairs.push((bytes, rank))
if rank > max_rank {
max_rank = rank
}
}
}
}
let n = max_rank + 1
let slots : Array[Bytes?] = Array::make(n, None)
let ranks : Map[Bytes, Int] = Map([])
for e in pairs {
let (bytes, rank) = e
slots[rank] = Some(bytes)
ranks.set(bytes, rank)
}
let tokens : Array[Bytes] = []
for i = 0; i < n; i = i + 1 {
match slots[i] {
Some(b) => tokens.push(b)
None => raise MalformedLine(0, "rank gap: no token for rank " + i.to_string())
}
}
build_vocab(tokens, ranks, specials)
}