use rustc_hash::FxHashMap;
use crate::AtomicRc;
use crate::arc::{Arc, ArcLabel};
use crate::error::OpenFstError;
use crate::fst::MutableFst;
use crate::properties::{K_FST_PROPERTIES, relabel_properties};
use crate::symbol_table::{K_NO_SYMBOL, SymbolTable};
pub fn relabel<A: Arc, F: MutableFst<A>>(
fst: &mut F,
ipairs: &[(A::Label, A::Label)],
opairs: &[(A::Label, A::Label)],
) -> Result<(), OpenFstError> {
let props = fst.properties(K_FST_PROPERTIES, false);
let input_map: FxHashMap<A::Label, A::Label> = ipairs.iter().copied().collect();
let output_map: FxHashMap<A::Label, A::Label> = opairs.iter().copied().collect();
let no_label = A::Label::no_label();
for (side, map) in [("Input", &input_map), ("Output", &output_map)] {
if let Some((from, _)) = map.iter().find(|(_, to)| **to == no_label) {
return Err(OpenFstError::InvalidOperation(format!(
"Relabel: {side} symbol ID {from} missing from target vocabulary"
)));
}
}
let states: Vec<A::StateId> = fst.states().collect();
for state in states {
fst.mutate_arcs(state, |arc| {
let ilabel = *input_map.get(&arc.ilabel()).unwrap_or(&arc.ilabel());
let olabel = *output_map.get(&arc.olabel()).unwrap_or(&arc.olabel());
if ilabel != arc.ilabel() || olabel != arc.olabel() {
*arc = A::new(ilabel, olabel, arc.weight().clone(), arc.nextstate());
}
});
}
fst.set_properties(relabel_properties(props), K_FST_PROPERTIES);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MissingSymbol<'a> {
#[default]
Refuse,
MapTo(&'a str),
}
pub struct RelabelSide<'a> {
pub old: &'a SymbolTable,
pub new: &'a AtomicRc<SymbolTable>,
pub missing: MissingSymbol<'a>,
pub attach: bool,
}
fn pairs<A: Arc>(side: &RelabelSide<'_>) -> Result<Vec<(A::Label, A::Label)>, OpenFstError> {
{
let unknown = match side.missing {
MissingSymbol::Refuse => None,
MissingSymbol::MapTo(symbol) => {
let label = side.new.find_key(symbol);
if label == K_NO_SYMBOL {
return Err(OpenFstError::SymbolTable(format!(
"Relabel: the symbol '{symbol}' offered for unknown symbols is itself \
missing from the target table"
)));
}
Some(label)
}
};
let mut pairs = Vec::new();
for item in side.old.iter() {
let new_label = match side.new.find_key(&item.symbol) {
K_NO_SYMBOL => match unknown {
Some(label) => label,
None => {
return Err(OpenFstError::SymbolTable(format!(
"Relabel: symbol '{}' (ID {}) is missing from the target table",
item.symbol, item.label
)));
}
},
label => label,
};
let (Some(from), Some(to)) = (
A::Label::from_i64(item.label),
A::Label::from_i64(new_label),
) else {
return Err(OpenFstError::SymbolTable(format!(
"Relabel: symbol '{}' has a label that does not fit the arc's label type",
item.symbol
)));
};
pairs.push((from, to));
}
Ok(pairs)
}
}
pub fn relabel_tables<A: Arc, F: MutableFst<A>>(
fst: &mut F,
input: Option<RelabelSide<'_>>,
output: Option<RelabelSide<'_>>,
) -> Result<(), OpenFstError> {
let ipairs = match &input {
Some(side) => pairs::<A>(side)?,
None => Vec::new(),
};
let opairs = match &output {
Some(side) => pairs::<A>(side)?,
None => Vec::new(),
};
relabel(fst, &ipairs, &opairs)?;
if let Some(side) = input
&& side.attach
{
fst.set_input_symbols(Some(AtomicRc::clone(side.new)));
}
if let Some(side) = output
&& side.attach
{
fst.set_output_symbols(Some(AtomicRc::clone(side.new)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arc::StdArc;
use crate::fst::{ExpandedFst as _, Fst as _};
use crate::fsts::vector_fst::StdVectorFst;
use crate::properties::{K_ACCEPTOR, K_NOT_ACCEPTOR};
use crate::weight::Weight;
use crate::weights::float_weight::TropicalWeight;
fn transducer() -> StdVectorFst {
let mut fst = StdVectorFst::new();
for _ in 0..3 {
fst.add_state();
}
fst.set_start(0);
fst.set_final(2, TropicalWeight::one());
fst.add_arc(0, StdArc::new(1, 10, TropicalWeight(1.0), 1));
fst.add_arc(1, StdArc::new(2, 20, TropicalWeight(2.0), 2));
fst
}
fn arcs(fst: &StdVectorFst) -> Vec<(i32, i32)> {
(0..fst.num_states() as i32)
.flat_map(|s| {
fst.arcs(s)
.map(|a| (a.ilabel(), a.olabel()))
.collect::<Vec<_>>()
})
.collect()
}
#[test]
fn labels_with_a_mapping_are_renamed_and_the_rest_are_left_alone() {
let mut fst = transducer();
relabel(&mut fst, &[(1, 100)], &[(20, 200)]).unwrap();
assert_eq!(arcs(&fst), vec![(100, 10), (2, 200)]);
}
#[test]
fn relabelling_leaves_the_structure_alone() {
let mut fst = transducer();
let before: Vec<(i32, i32)> = (0..fst.num_states() as i32)
.flat_map(|s| {
fst.arcs(s)
.map(move |a| (s, a.nextstate()))
.collect::<Vec<_>>()
})
.collect();
relabel(&mut fst, &[(1, 5), (2, 6)], &[(10, 50), (20, 60)]).unwrap();
assert_eq!(fst.num_states(), 3);
assert_eq!(fst.start(), Some(0));
assert_eq!(fst.final_weight(2), TropicalWeight::one());
let after: Vec<(i32, i32)> = (0..fst.num_states() as i32)
.flat_map(|s| {
fst.arcs(s)
.map(move |a| (s, a.nextstate()))
.collect::<Vec<_>>()
})
.collect();
assert_eq!(after, before);
}
#[test]
fn label_properties_are_given_up() {
let mut fst = transducer();
assert_ne!(fst.properties(K_NOT_ACCEPTOR, true) & K_NOT_ACCEPTOR, 0);
relabel(&mut fst, &[], &[(10, 1), (20, 2)]).unwrap();
assert_eq!(arcs(&fst), vec![(1, 1), (2, 2)]);
let props = fst.properties(K_FST_PROPERTIES, false);
assert_eq!(
props & (K_ACCEPTOR | K_NOT_ACCEPTOR),
0,
"acceptor-ness is no longer claimed either way"
);
assert_ne!(fst.properties(K_ACCEPTOR, true) & K_ACCEPTOR, 0);
}
#[test]
fn a_mapping_to_no_label_is_refused_before_anything_changes() {
let mut fst = transducer();
let before = arcs(&fst);
assert!(relabel(&mut fst, &[(2, -1)], &[]).is_err());
assert_eq!(arcs(&fst), before);
assert!(relabel(&mut fst, &[], &[(20, -1)]).is_err());
assert_eq!(arcs(&fst), before);
}
fn table(name: &str, symbols: &[(&str, i64)]) -> AtomicRc<SymbolTable> {
let mut table = SymbolTable::new(name.to_string());
for &(symbol, label) in symbols {
table.add_symbol(symbol, label);
}
AtomicRc::new(table)
}
#[test]
fn symbol_tables_decide_the_new_numbering() {
let old = table("old", &[("<eps>", 0), ("a", 1), ("b", 2)]);
let new = table("new", &[("<eps>", 0), ("b", 7), ("a", 9)]);
let mut fst = transducer();
relabel_tables(
&mut fst,
Some(RelabelSide {
old: &old,
new: &new,
missing: MissingSymbol::Refuse,
attach: true,
}),
None,
)
.unwrap();
assert_eq!(arcs(&fst), vec![(9, 10), (7, 20)]);
assert_eq!(
fst.input_symbols().unwrap().find_symbol(9),
Some("a"),
"the new table is attached"
);
}
#[test]
fn a_symbol_the_new_table_lacks_is_refused_or_mapped_to_the_unknown_one() {
let old = table("old", &[("<eps>", 0), ("a", 1), ("b", 2)]);
let new = table("new", &[("<eps>", 0), ("a", 9), ("<unk>", 99)]);
let mut fst = transducer();
let before = arcs(&fst);
assert!(
relabel_tables(
&mut fst,
Some(RelabelSide {
old: &old,
new: &new,
missing: MissingSymbol::Refuse,
attach: true,
}),
None,
)
.is_err(),
"'b' is missing from the new table"
);
assert_eq!(arcs(&fst), before, "nothing was changed");
relabel_tables(
&mut fst,
Some(RelabelSide {
old: &old,
new: &new,
missing: MissingSymbol::MapTo("<unk>"),
attach: false,
}),
None,
)
.unwrap();
assert_eq!(arcs(&fst), vec![(9, 10), (99, 20)]);
assert!(fst.input_symbols().is_none(), "the table was not attached");
}
#[test]
fn an_unknown_symbol_that_is_itself_missing_is_refused() {
let old = table("old", &[("<eps>", 0), ("a", 1), ("b", 2)]);
let new = table("new", &[("<eps>", 0), ("a", 9)]);
let mut fst = transducer();
assert!(
relabel_tables(
&mut fst,
Some(RelabelSide {
old: &old,
new: &new,
missing: MissingSymbol::MapTo("<unk>"),
attach: true,
}),
None,
)
.is_err()
);
}
}