Skip to main content

token_align/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4//! Maps overlaps between token sequences and between character positions.
5//!
6//! The default comparison form removes common subword markers and applies
7//! pinned Unicode compatibility decomposition. Use [`Exact`] or implement
8//! [`Normalizer`] when the input needs different comparison rules.
9
10extern crate alloc;
11
12use alloc::string::String;
13use alloc::vec;
14use alloc::vec::Vec;
15
16mod diff;
17mod unicode_tables;
18
19#[cfg(test)]
20mod unit_tests;
21
22/// One list of counterpart indices for every input token.
23pub type Alignment = Vec<Vec<usize>>;
24
25/// One list of counterpart indices for every input character.
26pub type CharMap = Vec<Vec<usize>>;
27
28/// Produces the comparison form of one token.
29///
30/// `out` is empty when this method starts. Implementations append the complete
31/// comparison form and may leave it empty when the token has no counterpart.
32/// A caller can also use a closure with the same arguments.
33///
34/// Byte-level token schemes often encode a non-ASCII character as several
35/// visible characters. A normalizer for such a scheme can decode the token
36/// before appending its comparison form.
37///
38/// # Examples
39///
40/// This normalizer decodes one visible byte-level spelling before alignment.
41///
42/// ```
43/// use token_align::{get_alignments_with, Normalizer};
44///
45/// struct DecodeByteToken;
46///
47/// impl Normalizer for DecodeByteToken {
48///     fn normalize(&self, token: &str, out: &mut String) {
49///         if token == "é" {
50///             out.push('é');
51///         } else {
52///             out.push_str(token);
53///         }
54///     }
55/// }
56///
57/// let encoded = ["caf", "é"];
58/// let text = ["café"];
59/// assert_eq!(
60///     get_alignments_with(&encoded, &text, &DecodeByteToken),
61///     (vec![vec![0], vec![0]], vec![vec![0, 1]])
62/// );
63/// ```
64pub trait Normalizer {
65    /// Appends the comparison form of `token` to `out`.
66    fn normalize(&self, token: &str, out: &mut String);
67}
68
69impl<F> Normalizer for F
70where
71    F: Fn(&str, &mut String),
72{
73    fn normalize(&self, token: &str, out: &mut String) {
74        self(token, out);
75    }
76}
77
78/// The default marker removal and Unicode comparison form.
79///
80/// The fixed control tokens `[CLS]`, `[SEP]`, `[PAD]`, `[UNK]`, `[MASK]`,
81/// `<s>`, `</s>`, `<pad>`, `<unk>`, `<mask>`, `<cls>`, `<sep>`,
82/// `<|endoftext|>`, and `<|startoftext|>` produce an empty comparison form.
83///
84/// One leading `##` and one trailing `</w>` are removed. Every `▁`, `Ġ`, and
85/// `Ċ` is dropped. `Ġ` and `Ċ` are also Maltese capital letters, so literal
86/// Maltese text containing either character loses it under this comparison
87/// form. Use a custom [`Normalizer`] when those letters carry text.
88///
89/// Each remaining character receives recursive compatibility decomposition
90/// from the Unicode Character Database 17.0.0 data. The decomposition is
91/// canonically ordered within that character, then mapped with the simple
92/// lowercase field. Final sigma `ς` maps to sigma `σ`. Characters are folded
93/// independently, so marks are not reordered across input characters.
94#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
95pub struct Fold;
96
97impl Normalizer for Fold {
98    fn normalize(&self, token: &str, out: &mut String) {
99        if is_special(token) {
100            return;
101        }
102
103        let token = token.strip_prefix("##").unwrap_or(token);
104        let token = token.strip_suffix("</w>").unwrap_or(token);
105        for c in token.chars() {
106            if !matches!(c, '\u{2581}' | '\u{0120}' | '\u{010A}') {
107                unicode_tables::push_folded(c, out);
108            }
109        }
110    }
111}
112
113/// A comparison form that copies each token unchanged.
114#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub struct Exact;
116
117impl Normalizer for Exact {
118    fn normalize(&self, token: &str, out: &mut String) {
119        out.push_str(token);
120    }
121}
122
123/// Aligns two token sequences with the default [`Fold`] comparison form.
124///
125/// The first result maps `a` indices to `b` indices. The second result maps
126/// `b` indices to `a` indices.
127///
128/// # Examples
129///
130/// ```
131/// use token_align::get_alignments;
132///
133/// let a = ["New York"];
134/// let b = ["New", "York"];
135/// assert_eq!(
136///     get_alignments(&a, &b),
137///     (vec![vec![0, 1]], vec![vec![0], vec![0]])
138/// );
139/// ```
140pub fn get_alignments<A, B>(a: &[A], b: &[B]) -> (Alignment, Alignment)
141where
142    A: AsRef<str>,
143    B: AsRef<str>,
144{
145    get_alignments_with(a, b, &Fold)
146}
147
148/// Aligns two token sequences with a caller-provided comparison form.
149pub fn get_alignments_with<A, B, N>(a: &[A], b: &[B], norm: &N) -> (Alignment, Alignment)
150where
151    A: AsRef<str>,
152    B: AsRef<str>,
153    N: Normalizer + ?Sized,
154{
155    let left = Stream::from_tokens(a, norm);
156    let right = Stream::from_tokens(b, norm);
157    align_streams(&left, &right, a.len(), b.len())
158}
159
160/// Maps Unicode scalar positions in `a` to positions in `b` and back.
161///
162/// # Examples
163///
164/// ```
165/// use token_align::get_charmap;
166///
167/// assert_eq!(get_charmap("bar", "Bar"), (vec![vec![0], vec![1], vec![2]], vec![vec![0], vec![1], vec![2]]));
168/// ```
169pub fn get_charmap(a: &str, b: &str) -> (CharMap, CharMap) {
170    get_charmap_with(a, b, &Fold)
171}
172
173/// Maps Unicode scalar positions with a caller-provided comparison form.
174pub fn get_charmap_with<N>(a: &str, b: &str, norm: &N) -> (CharMap, CharMap)
175where
176    N: Normalizer + ?Sized,
177{
178    let left = Stream::from_chars(a, norm);
179    let right = Stream::from_chars(b, norm);
180    align_streams(&left, &right, a.chars().count(), b.chars().count())
181}
182
183fn is_special(token: &str) -> bool {
184    matches!(
185        token,
186        "[CLS]"
187            | "[SEP]"
188            | "[PAD]"
189            | "[UNK]"
190            | "[MASK]"
191            | "<s>"
192            | "</s>"
193            | "<pad>"
194            | "<unk>"
195            | "<mask>"
196            | "<cls>"
197            | "<sep>"
198            | "<|endoftext|>"
199            | "<|startoftext|>"
200    )
201}
202
203struct Stream {
204    chars: Vec<char>,
205    owners: Vec<usize>,
206    units: Vec<Unit>,
207}
208
209impl Stream {
210    fn from_tokens<T, N>(tokens: &[T], norm: &N) -> Self
211    where
212        T: AsRef<str>,
213        N: Normalizer + ?Sized,
214    {
215        let estimated = tokens.iter().fold(0usize, |sum, token| {
216            sum.saturating_add(token.as_ref().len())
217        });
218        let mut stream = Self {
219            chars: Vec::with_capacity(estimated),
220            owners: Vec::with_capacity(estimated),
221            units: Vec::new(),
222        };
223        let mut scratch = String::new();
224        for (index, token) in tokens.iter().enumerate() {
225            scratch.clear();
226            norm.normalize(token.as_ref(), &mut scratch);
227            stream.extend(&scratch, index);
228        }
229        stream.group_units();
230        stream
231    }
232
233    fn from_chars<N>(text: &str, norm: &N) -> Self
234    where
235        N: Normalizer + ?Sized,
236    {
237        let count = text.chars().count();
238        let mut stream = Self {
239            chars: Vec::with_capacity(count),
240            owners: Vec::with_capacity(count),
241            units: Vec::new(),
242        };
243        let mut scratch = String::new();
244        for (index, (start, c)) in text.char_indices().enumerate() {
245            let end = start + c.len_utf8();
246            scratch.clear();
247            norm.normalize(&text[start..end], &mut scratch);
248            stream.extend(&scratch, index);
249        }
250        stream.group_units();
251        stream
252    }
253
254    fn extend(&mut self, text: &str, owner: usize) {
255        for c in text.chars() {
256            self.chars.push(c);
257            self.owners.push(owner);
258        }
259    }
260
261    fn group_units(&mut self) {
262        for (index, &c) in self.chars.iter().enumerate() {
263            if self.units.is_empty() || !unicode_tables::is_extender(c) {
264                self.units.push(Unit {
265                    start: index,
266                    end: index + 1,
267                });
268            } else if let Some(unit) = self.units.last_mut() {
269                unit.end = index + 1;
270            }
271        }
272    }
273
274    fn keys(&self) -> impl ExactSizeIterator<Item = char> + '_ {
275        self.units.iter().map(|unit| self.chars[unit.start])
276    }
277}
278
279#[derive(Clone, Copy)]
280struct Unit {
281    start: usize,
282    end: usize,
283}
284
285fn align_streams(
286    left: &Stream,
287    right: &Stream,
288    left_len: usize,
289    right_len: usize,
290) -> (Alignment, Alignment) {
291    let left_keys: Vec<char> = left.keys().collect();
292    let right_keys: Vec<char> = right.keys().collect();
293    let mut unit_pairs = Vec::with_capacity(left_keys.len().min(right_keys.len()));
294    diff::matched_pairs(&left_keys, &right_keys, &mut unit_pairs);
295
296    let mut left_to_right = vec![Vec::new(); left_len];
297    let mut right_to_left = vec![Vec::new(); right_len];
298    for (left_unit, right_unit) in unit_pairs {
299        project_unit(
300            left,
301            left_unit,
302            right,
303            right_unit,
304            &mut left_to_right,
305            &mut right_to_left,
306        );
307    }
308    (left_to_right, right_to_left)
309}
310
311fn project_unit(
312    left: &Stream,
313    left_index: usize,
314    right: &Stream,
315    right_index: usize,
316    left_to_right: &mut Alignment,
317    right_to_left: &mut Alignment,
318) {
319    let left_unit = left.units[left_index];
320    let right_unit = right.units[right_index];
321    project_pair(
322        left,
323        left_unit.start,
324        right,
325        right_unit.start,
326        left_to_right,
327        right_to_left,
328    );
329
330    let mut right_cursor = right_unit.start + 1;
331    for left_char in left_unit.start + 1..left_unit.end {
332        while right_cursor < right_unit.end && right.chars[right_cursor] != left.chars[left_char] {
333            right_cursor += 1;
334        }
335        if right_cursor < right_unit.end {
336            project_pair(
337                left,
338                left_char,
339                right,
340                right_cursor,
341                left_to_right,
342                right_to_left,
343            );
344            right_cursor += 1;
345        }
346    }
347}
348
349fn project_pair(
350    left: &Stream,
351    left_char: usize,
352    right: &Stream,
353    right_char: usize,
354    left_to_right: &mut Alignment,
355    right_to_left: &mut Alignment,
356) {
357    let left_owner = left.owners[left_char];
358    let right_owner = right.owners[right_char];
359    if left_owner >= left_to_right.len() || right_owner >= right_to_left.len() {
360        return;
361    }
362    push_new(&mut left_to_right[left_owner], right_owner);
363    push_new(&mut right_to_left[right_owner], left_owner);
364}
365
366fn push_new(indices: &mut Vec<usize>, index: usize) {
367    if indices.last().copied() != Some(index) {
368        indices.push(index);
369    }
370}