#![allow(missing_docs)]
use crate::phoneme::PhonemeTab;
pub const INSTN_RETURN: u16 = 0x0001;
pub const INSTN_CONTINUE: u16 = 0x0002;
pub const I_IPA_NAME: u16 = 0x0d; pub const I_CHANGE_PHONEME: u16 = 0x01; pub const I_REPLACE_NEXT_PHONEME: u16 = 0x02;
pub const I_INSERT_PHONEME: u16 = 0x03;
pub const I_APPEND_PHONEME: u16 = 0x04;
pub const I_APPEND_IFNEXTVOWEL: u16 = 0x05;
pub const I_SET_LENGTH: u16 = 0x0a;
pub const I_ADD_LENGTH: u16 = 0x0c;
pub const I_PAUSE_BEFORE: u16 = 0x07;
pub const I_PAUSE_AFTER: u16 = 0x08;
pub const I_CALLPH: u16 = 0x9100;
pub const I_PITCHENV: u16 = 0x9200;
pub const I_AMPENV: u16 = 0x9300;
pub const I_VOWELIN: u16 = 0xa100;
pub const I_VOWELOUT: u16 = 0xa200;
pub const I_FMT: u16 = 0xb000;
pub const I_WAV: u16 = 0xc000;
pub const I_VWLSTART: u16 = 0xd000;
pub const I_VWLENDING: u16 = 0xe000;
pub const I_WAVADD: u16 = 0xf000;
pub fn num_instn_words(instn: u16) -> usize {
const N_WORDS: [u8; 16] = [0, 1, 0, 0, 1, 1, 0, 1, 1, 2, 4, 0, 0, 0, 0, 0];
let hi4 = (instn >> 12) as usize;
let n = N_WORDS[hi4];
if n > 0 {
return n as usize;
}
match hi4 {
0 => {
let opcode = (instn >> 8) as u8;
if opcode == I_IPA_NAME as u8 {
let data = (instn & 0xff) as usize; 1 + (data + 1) / 2 } else {
1
}
}
2 | 3 => {
let n = instn & 0x0f00;
if n == 0x0600 || n == 0x0d00 { 2 } else { 1 }
}
6 => {
let type2 = (instn & 0x0f00) >> 9;
if type2 == 5 || type2 == 6 { 12 } else { 1 }
}
0xb | 0xc | 0xd | 0xe | 0xf => 2,
_ => 1,
}
}
#[derive(Debug, Clone, Default)]
pub struct PhonemeExtract {
pub fmt_addr: Option<u32>,
pub fmt_param: i8,
pub wav_addr: Option<u32>,
pub wav_param: i8,
pub vwlstart_addr: Option<u32>,
pub vwlending_addr: Option<u32>,
pub change_phoneme_code: Option<u8>,
pub insert_phoneme: Option<u8>,
pub append_phoneme: Option<u8>,
pub append_if_next_vowel: Option<u8>,
pub replace_next_phoneme: Option<u8>,
pub pause_before_ms: u8,
pub pause_after_ms: u8,
pub set_length: Option<u8>,
pub add_length: i8,
#[allow(dead_code)]
pub vowel_transition: [u32; 4],
pub pd_fornextph: bool,
pub ipa_string: Option<String>,
}
pub fn select_vowel_start(program: u16, phonindex: &[u8], next_start_type: u8) -> Option<u32> {
const PHON_VOWEL_TYPES: i32 = 28;
if program == 0 {
return None;
}
let voweltype = next_start_type as i32 - PHON_VOWEL_TYPES;
if !(0..6).contains(&voweltype) {
return None;
}
let voweltype = voweltype as usize;
let max_words = phonindex.len() / 2;
let word = |i: usize| -> u16 {
u16::from_le_bytes([phonindex[i * 2], phonindex[i * 2 + 1]])
};
let mut pc = program as usize;
let scan_limit = pc + 128;
while pc < max_words && pc < scan_limit {
let instn = word(pc);
if instn == INSTN_RETURN {
break;
}
if instn >> 12 == 6 && ((instn & 0x0f00) >> 9) == 5 {
let base = pc + voweltype * 2;
if base + 2 < max_words {
let p1 = word(base + 1) as u32;
let p2 = word(base + 2) as u32;
return Some((((p1 & 0xf) << 16) + p2) * 4);
}
return None;
}
pc += num_instn_words(instn);
}
None
}
pub fn scan_phoneme(program: u16, phonindex: &[u8]) -> PhonemeExtract {
let mut result = PhonemeExtract::default();
if program == 0 {
return result;
}
let mut pc = program as usize; let max_words = phonindex.len() / 2;
let scan_limit = pc + 128;
loop {
if pc >= max_words || pc >= scan_limit {
break;
}
let byte_off = pc * 2;
let instn = u16::from_le_bytes([phonindex[byte_off], phonindex[byte_off + 1]]);
if instn == INSTN_RETURN {
break;
}
let hi4 = instn >> 12;
match hi4 {
0xb => {
if result.fmt_addr.is_none() && pc + 1 < max_words {
let next = u16::from_le_bytes([
phonindex[(pc+1)*2],
phonindex[(pc+1)*2 + 1],
]);
let addr = ((instn & 0xf) as u32) << 18 | ((next as u32) << 2);
result.fmt_addr = Some(addr);
result.fmt_param = ((instn >> 4) & 0xff) as i8;
}
if pc + 2 < max_words {
let next2 = u16::from_le_bytes([
phonindex[(pc + 2) * 2],
phonindex[(pc + 2) * 2 + 1],
]);
if next2 >> 12 == 0xf {
pc += 2;
continue;
}
}
break;
}
0xc => {
if result.wav_addr.is_none() && pc + 1 < max_words {
let next = u16::from_le_bytes([
phonindex[(pc+1)*2],
phonindex[(pc+1)*2 + 1],
]);
let addr = ((instn & 0xf) as u32) << 18 | ((next as u32) << 2);
result.wav_addr = Some(addr);
result.wav_param = ((instn >> 4) & 0xff) as i8;
}
break; }
0xd => {
if result.vwlstart_addr.is_none() && pc + 1 < max_words {
let next = u16::from_le_bytes([
phonindex[(pc+1)*2],
phonindex[(pc+1)*2 + 1],
]);
result.vwlstart_addr = Some(
((instn & 0xf) as u32) << 18 | ((next as u32) << 2)
);
}
pc += 2;
continue;
}
0xe => {
if result.vwlending_addr.is_none() && pc + 1 < max_words {
let next = u16::from_le_bytes([
phonindex[(pc+1)*2],
phonindex[(pc+1)*2 + 1],
]);
result.vwlending_addr = Some(
((instn & 0xf) as u32) << 18 | ((next as u32) << 2)
);
}
pc += 2;
continue;
}
0xf => {
if result.wav_addr.is_none() && pc + 1 < max_words {
let next = u16::from_le_bytes([
phonindex[(pc+1)*2],
phonindex[(pc+1)*2 + 1],
]);
result.wav_addr = Some(
((instn & 0xf) as u32) << 18 | ((next as u32) << 2)
);
}
break;
}
9 => {
let instn2 = ((instn >> 8) & 0xf) as u8;
if instn2 == 1 && pc + 1 < max_words {
let next = u16::from_le_bytes([phonindex[(pc+1)*2], phonindex[(pc+1)*2+1]]);
let called_prog = ((((instn & 0xf) as u32) << 16) | next as u32) as usize;
if called_prog > 0 && called_prog < max_words {
let sub = scan_phoneme(called_prog as u16, phonindex);
if result.fmt_addr.is_none() { result.fmt_addr = sub.fmt_addr; result.fmt_param = sub.fmt_param; }
if result.wav_addr.is_none() { result.wav_addr = sub.wav_addr; result.wav_param = sub.wav_param; }
if result.vwlstart_addr.is_none() { result.vwlstart_addr = sub.vwlstart_addr; }
if result.vwlending_addr.is_none() { result.vwlending_addr = sub.vwlending_addr; }
for k in 0..4 {
if result.vowel_transition[k] == 0 {
result.vowel_transition[k] = sub.vowel_transition[k];
}
}
}
}
pc += 2;
continue;
}
0 => {
let opcode = (instn >> 8) as u8;
if opcode == I_CHANGE_PHONEME as u8 {
result.change_phoneme_code = Some((instn & 0xff) as u8);
}
pc += num_instn_words(instn);
continue;
}
0xa => {
if pc + 3 < max_words {
let w = |o: usize| {
u16::from_le_bytes([phonindex[(pc + o) * 2], phonindex[(pc + o) * 2 + 1]])
as u32
};
let t0 = ((instn as u32 & 0xff) << 16) | w(1);
let t1 = (w(2) << 16) | w(3);
match (instn >> 8) & 0xf {
1 => { result.vowel_transition[0] = t0;
result.vowel_transition[1] = t1;
}
2 => { result.vowel_transition[2] = t0;
result.vowel_transition[3] = t1;
}
_ => {}
}
}
pc += 4;
continue;
}
_ => {
pc += num_instn_words(instn);
continue;
}
}
}
result
}
const COND_MARK: u16 = 0x2000; const COND_OR_FLAG: u16 = 0x1000;
const INSTN_NOT: u16 = 0x0003;
const JUMP_FALSE_MASK: u16 = 0xf800;
const JUMP_FALSE: u16 = 0x6800;
const COND_IS_TYPE: u16 = 0x00;
const COND_IS_PLACE: u16 = 0x20;
const COND_IS_FLAG: u16 = 0x40;
const COND_IS_OTHER: u16 = 0x80;
const OTHER_IS_AFTER_STRESS: u16 = 9;
const OTHER_IS_NOT_VOWEL: u16 = 10;
const OTHER_IS_FINAL_VOWEL: u16 = 11;
const OTHER_IS_VOICED: u16 = 12;
const OTHER_IS_FIRST_VOWEL: u16 = 13;
const OTHER_IS_SECOND_VOWEL: u16 = 14;
const OTHER_IS_TRANSLATION: u16 = 16;
const OTHER_IS_BREAK: u16 = 17;
const OTHER_IS_WORD_START: u16 = 18;
const OTHER_IS_WORD_END: u16 = 19;
const PH_PAUSE: u8 = 0;
const PH_VOWEL: u8 = 2;
const PH_LIQUID: u8 = 3;
const PH_VOICED_FLAG: u32 = 1 << 4;
const PHON_VOWEL_TYPES: i32 = 28;
#[derive(Debug, Clone)]
pub struct Neighbours {
pub prev: u8,
pub this: u8,
pub next: u8,
pub next2: u8,
pub stress: u8,
pub next_stress: u8,
pub prev_stress: u8,
pub next2_stress: u8,
pub wordstress: u8,
pub vowel_position: u8,
pub reduce: u32,
pub translation_given: bool,
pub this_wordstart: bool,
pub prev_wordstart: bool,
pub next_wordstart: bool,
pub next2_wordstart: bool,
}
pub const STRESS_UNKNOWN: u8 = u8::MAX;
impl Default for Neighbours {
fn default() -> Self {
Neighbours {
prev: 0,
this: 0,
next: 0,
next2: 0,
stress: 0,
next_stress: STRESS_UNKNOWN,
prev_stress: STRESS_UNKNOWN,
next2_stress: STRESS_UNKNOWN,
vowel_position: 0,
wordstress: STRESS_UNKNOWN,
reduce: 0,
translation_given: false,
this_wordstart: false,
prev_wordstart: false,
next_wordstart: false,
next2_wordstart: false,
}
}
}
#[inline]
fn word_at(phonindex: &[u8], i: usize) -> u16 {
let b = i * 2;
if b + 1 < phonindex.len() {
u16::from_le_bytes([phonindex[b], phonindex[b + 1]])
} else {
0
}
}
pub fn interpret_phoneme<F>(
program: u16,
phonindex: &[u8],
nb: &Neighbours,
lookup: F,
) -> PhonemeExtract
where
F: Fn(u8) -> Option<PhonemeTab>,
{
interpret_phoneme_ctl(program, phonindex, nb, lookup, false)
}
pub fn interpret_phoneme_ctl<F>(
program: u16,
phonindex: &[u8],
nb: &Neighbours,
lookup: F,
phoneme_list_mode: bool,
) -> PhonemeExtract
where
F: Fn(u8) -> Option<PhonemeTab>,
{
let mut result = PhonemeExtract::default();
if program == 0 {
return result;
}
let max_words = phonindex.len() / 2;
let mut sound_addr: [u32; 5] = [0; 5];
let mut sound_param: [i8; 5] = [0; 5];
let mut change_phoneme: Option<u8> = None;
let mut vowel_transition: [u32; 4] = [0; 4];
const N_RETURN: usize = 10;
let mut return_stack: [usize; N_RETURN] = [0; N_RETURN];
let mut n_return = 0usize;
let mut pc = program as usize;
let mut end_flag: i32 = 0;
let mut guard = 0usize;
while end_flag != 1 {
if pc >= max_words {
break;
}
guard += 1;
if guard > 8192 {
break; }
let instn = word_at(phonindex, pc);
let hi4 = instn >> 12;
let instn2 = ((instn >> 8) & 0xf) as usize;
match hi4 {
0 => {
let data = (instn & 0xff) as usize;
if instn2 == 0 {
match data {
0x0001 => end_flag = 1,
_ => {} }
pc += 1;
} else if instn2 == I_IPA_NAME as usize {
let n = data.min(16);
let mut bytes = Vec::with_capacity(n);
for j in 0..n.div_ceil(2) {
let w = word_at(phonindex, pc + 1 + j);
bytes.push((w >> 8) as u8);
bytes.push((w & 0xff) as u8);
}
bytes.truncate(n);
result.ipa_string = String::from_utf8(bytes).ok();
pc += 1 + (data + 1) / 2; } else {
let param = (instn & 0xff) as u8;
match instn2 as u16 {
I_CHANGE_PHONEME => {
change_phoneme = Some(param);
if phoneme_list_mode {
end_flag = 1;
}
}
I_REPLACE_NEXT_PHONEME => result.replace_next_phoneme = Some(param),
I_INSERT_PHONEME => result.insert_phoneme = Some(param),
I_APPEND_PHONEME => result.append_phoneme = Some(param),
I_APPEND_IFNEXTVOWEL => result.append_if_next_vowel = Some(param),
I_SET_LENGTH => result.set_length = Some(param),
I_ADD_LENGTH => result.add_length = param as i8,
I_PAUSE_BEFORE => result.pause_before_ms = param,
I_PAUSE_AFTER => result.pause_after_ms = param,
_ => {}
}
pc += 1;
}
}
1 => {
if phoneme_list_mode && instn2 < 8 {
if stress_condition(nb, (instn2 & 7) as u16, 1, true, &lookup) {
change_phoneme = Some((instn & 0xff) as u8);
end_flag = 1; }
}
pc += 1;
}
2 | 3 => {
let mut or_flag = false;
let mut truth = true;
let mut cinstn = instn;
while (cinstn & 0xe000) == COND_MARK {
let mut t2 = interpret_condition(pc, phonindex, nb, &lookup);
pc += num_instn_words(cinstn);
if word_at(phonindex, pc) == INSTN_NOT {
t2 = !t2;
pc += 1;
}
truth = if or_flag { truth || t2 } else { truth && t2 };
or_flag = (cinstn & COND_OR_FLAG) != 0;
cinstn = word_at(phonindex, pc);
}
if !truth {
if (cinstn & JUMP_FALSE_MASK) == JUMP_FALSE {
pc += (cinstn & 0xff) as usize;
} else {
pc += num_instn_words(cinstn);
if (word_at(phonindex, pc) & 0xfe00) == 0x6000 {
pc += 1; }
}
}
}
6 => {
match instn2 >> 1 {
0 => {
pc = pc.wrapping_add((instn & 0xff) as usize);
}
5 => {
result.pd_fornextph = true;
if let Some((addr, param)) = switch_on_vowel_type(
phonindex, pc, lookup(nb.next).map(|p| p.start_type),
) {
sound_addr[2] = addr;
sound_param[2] = param;
}
pc += 13; }
6 => {
if let Some((addr, param)) = switch_on_vowel_type(
phonindex, pc, lookup(nb.prev).map(|p| p.end_type),
) {
sound_addr[3] = addr;
sound_param[3] = param;
}
pc += 13;
}
_ => {
pc += 1;
}
}
}
9 => {
let data = (((instn & 0xf) as usize) << 16) | word_at(phonindex, pc + 1) as usize;
match instn2 {
1 => {
if n_return < N_RETURN && data > 0 && data < max_words {
return_stack[n_return] = pc + 2;
n_return += 1;
pc = data;
} else {
pc += 2;
}
}
_ => {
pc += 2;
}
}
}
0xa => {
let w = |o: usize| word_at(phonindex, pc + o) as u32;
let t0 = ((instn as u32 & 0xff) << 16) | w(1);
let t1 = (w(2) << 16) | w(3);
match instn2 {
1 => {
vowel_transition[0] = t0;
vowel_transition[1] = t1;
}
2 => {
vowel_transition[2] = t0;
vowel_transition[3] = t1;
}
_ => {}
}
pc += 4;
}
0xb | 0xc | 0xd | 0xe | 0xf => {
let idx = (hi4 - 0xb) as usize; let addr = ((instn & 0xf) as u32) << 18 | ((word_at(phonindex, pc + 1) as u32) << 2);
sound_addr[idx] = addr;
sound_param[idx] = ((instn >> 4) & 0xff) as i8; let after = word_at(phonindex, pc + 2);
if after != INSTN_CONTINUE {
if idx < 2 {
end_flag = 1;
if (after >> 12) == 0xf {
end_flag = 2; }
} else if idx == 4 {
end_flag -= 1;
}
}
pc += 2;
}
_ => {
pc += num_instn_words(instn).max(1);
}
}
if end_flag == 1 && n_return > 0 {
end_flag = 0;
n_return -= 1;
pc = return_stack[n_return];
}
}
if sound_addr[0] != 0 {
result.fmt_addr = Some(sound_addr[0]);
result.fmt_param = sound_param[0];
}
if sound_addr[1] != 0 {
result.wav_addr = Some(sound_addr[1]);
result.wav_param = sound_param[1];
} else if sound_addr[4] != 0 {
result.wav_addr = Some(sound_addr[4]);
result.wav_param = sound_param[4];
}
if sound_addr[2] != 0 {
result.vwlstart_addr = Some(sound_addr[2]);
}
if sound_addr[3] != 0 {
result.vwlending_addr = Some(sound_addr[3]);
}
result.change_phoneme_code = change_phoneme;
result.vowel_transition = vowel_transition;
result
}
fn switch_on_vowel_type(
phonindex: &[u8],
pc: usize,
voweltype_raw: Option<u8>,
) -> Option<(u32, i8)> {
let vt = voweltype_raw? as i32 - PHON_VOWEL_TYPES;
if !(0..6).contains(&vt) {
return None;
}
let base = pc + (vt as usize) * 2;
let p1 = word_at(phonindex, base + 1) as u32;
let p2 = word_at(phonindex, base + 2) as u32;
let addr = (((p1 & 0xf) << 16) + p2) * 4;
let param = ((p1 >> 4) & 0xff) as i8;
Some((addr, param))
}
fn interpret_condition<F>(
pc: usize,
phonindex: &[u8],
nb: &Neighbours,
lookup: &F,
) -> bool
where
F: Fn(u8) -> Option<PhonemeTab>,
{
let resolve = |code: u8| -> PhonemeTab {
if code == 0 {
PhonemeTab::default() } else {
lookup(code).unwrap_or_default()
}
};
let instn = word_at(phonindex, pc) & 0xfff;
let mut data = instn & 0xff;
let instn2 = (instn >> 8) as usize;
if instn2 >= 14 {
return false;
}
let mut which = instn2 % 7;
if which == 6 {
which = word_at(phonindex, pc + 1) as usize;
}
let mut check_endtype = false;
let code: u8 = match which {
0 => {
check_endtype = true;
nb.prev
}
5 => {
if nb.this_wordstart {
return false; }
check_endtype = true;
nb.prev
}
1 => nb.this,
2 => nb.next,
4 => {
if nb.next_wordstart {
return false; }
nb.next
}
3 => nb.next2,
6 => {
if nb.next_wordstart || nb.next2_wordstart {
return false; }
nb.next2
}
7 => {
if nb.next_wordstart {
return false;
}
if resolve(nb.next).typ == PH_VOWEL {
nb.next
} else {
if nb.next2_wordstart {
return false;
}
if resolve(nb.next2).typ == PH_VOWEL {
nb.next2
} else {
return false; }
}
}
8 => {
if resolve(nb.prev).typ == PH_VOWEL {
check_endtype = true;
nb.prev
} else {
return false;
}
}
_ => return false,
};
let ph = resolve(code);
if instn2 < 7 {
if let Some(target) = lookup(data as u8) {
if target.mnemonic != 0 && target.mnemonic == ph.mnemonic {
return true;
}
}
if check_endtype && ph.typ == PH_VOWEL {
return data as u8 == ph.end_type; }
return data as u8 == ph.start_type; }
data = instn & 0x1f;
match instn & 0xe0 {
COND_IS_TYPE => ph.typ as u16 == data,
COND_IS_PLACE => ((ph.phflags >> 16) & 0xf) as u16 == data,
COND_IS_FLAG => (ph.phflags & (1u32 << data)) != 0,
COND_IS_OTHER => match data {
0..=4 => stress_condition(nb, data, which, false, &lookup),
OTHER_IS_AFTER_STRESS => false, OTHER_IS_NOT_VOWEL => ph.typ != PH_VOWEL,
OTHER_IS_FINAL_VOWEL => {
let n = resolve(nb.next);
let n2 = resolve(nb.next2);
!(n.typ == PH_VOWEL || n2.typ == PH_VOWEL)
}
OTHER_IS_VOICED => {
ph.typ == PH_VOWEL || ph.typ == PH_LIQUID || (ph.phflags & PH_VOICED_FLAG) != 0
}
OTHER_IS_BREAK => ph.typ == PH_PAUSE,
OTHER_IS_WORD_START => match which {
0 | 5 => nb.prev_wordstart,
1 => nb.this_wordstart,
2 | 4 => nb.next_wordstart,
3 | 6 => nb.next2_wordstart,
_ => false,
},
OTHER_IS_WORD_END => {
match which {
1 => nb.next == 0 || nb.next_wordstart || resolve(nb.next).typ == PH_PAUSE,
0 | 5 => nb.this_wordstart,
_ => false,
}
}
OTHER_IS_TRANSLATION => which == 1 && nb.translation_given,
OTHER_IS_FIRST_VOWEL => which == 1 && nb.vowel_position == 1,
OTHER_IS_SECOND_VOWEL => which == 1 && nb.vowel_position == 2,
_ => false,
},
_ => false,
}
}
fn stress_condition<F>(
nb: &Neighbours,
condition: u16,
which: usize,
is_change_if: bool,
lookup: &F,
) -> bool
where
F: Fn(u8) -> Option<PhonemeTab>,
{
const CONDITION_LEVEL: [u16; 4] = [1, 2, 4, 15];
let is_vowel = |c: u8| lookup(c).is_some_and(|p| p.typ == PH_VOWEL);
let known = |v: u8| (v != STRESS_UNKNOWN).then_some(v);
let selected = match which {
0 | 5 | 8 => known(nb.prev_stress),
2 | 4 => known(nb.next_stress),
3 | 6 => known(nb.next2_stress),
7 => {
if is_vowel(nb.next) {
known(nb.next_stress)
} else {
known(nb.next2_stress)
}
}
_ => None, };
let mut level = if let Some(v) = selected {
v
} else if is_vowel(nb.this) {
nb.stress
} else if nb.next_stress == STRESS_UNKNOWN {
nb.stress
} else if is_vowel(nb.next) {
nb.next_stress
} else {
return false; };
if is_change_if && nb.translation_given && nb.reduce & 1 == 0 {
return false;
}
let wordstress = if nb.wordstress == STRESS_UNKNOWN { 4 } else { nb.wordstress };
if nb.reduce & 2 != 0 && level >= wordstress {
level = 4; }
let level = (level as u16) & 0xf;
match condition {
4 => level >= wordstress as u16,
3 => level > 3,
c => level < CONDITION_LEVEL[(c & 3) as usize],
}
}
#[derive(Debug, Clone, Copy)]
pub enum ProgInstn {
Fmt { addr: u32, param: i8 },
Wav { addr: u32 },
VwlStart { addr: u32 },
VwlEnding { addr: u32 },
CallPh { program: u16 },
Continue,
Raw(u16),
Return,
}
pub fn compile_program(instns: &[ProgInstn]) -> Vec<u8> {
let mut out = Vec::new();
for instn in instns {
match *instn {
ProgInstn::Fmt { addr, param } => {
let word = I_FMT
| (((param as u8) as u16) << 4)
| (((addr >> 18) & 0xf) as u16);
out.extend_from_slice(&word.to_le_bytes());
out.extend_from_slice(&(((addr >> 2) & 0xffff) as u16).to_le_bytes());
}
ProgInstn::Wav { addr } => {
out.extend_from_slice(&(I_WAV | (((addr >> 18) & 0xf) as u16)).to_le_bytes());
out.extend_from_slice(&(((addr >> 2) & 0xffff) as u16).to_le_bytes());
}
ProgInstn::VwlStart { addr } => {
out.extend_from_slice(&(I_VWLSTART | (((addr >> 18) & 0xf) as u16)).to_le_bytes());
out.extend_from_slice(&(((addr >> 2) & 0xffff) as u16).to_le_bytes());
}
ProgInstn::VwlEnding { addr } => {
out.extend_from_slice(&(I_VWLENDING | (((addr >> 18) & 0xf) as u16)).to_le_bytes());
out.extend_from_slice(&(((addr >> 2) & 0xffff) as u16).to_le_bytes());
}
ProgInstn::CallPh { program } => {
out.extend_from_slice(&I_CALLPH.to_le_bytes());
out.extend_from_slice(&program.to_le_bytes());
}
ProgInstn::Continue => out.extend_from_slice(&INSTN_CONTINUE.to_le_bytes()),
ProgInstn::Raw(w) => out.extend_from_slice(&w.to_le_bytes()),
ProgInstn::Return => out.extend_from_slice(&INSTN_RETURN.to_le_bytes()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compile_program_round_trip() {
let addr = 0x1234u32 * 4; let param = 42i8;
let bytecode = compile_program(&[ProgInstn::Fmt { addr, param }, ProgInstn::Return]);
let phonindex = pad(&bytecode);
let r = scan_phoneme(PROG, &phonindex);
assert_eq!(r.fmt_addr, Some(addr), "FMT address did not round-trip");
assert_eq!(r.fmt_param, param, "FMT param did not round-trip");
assert!(r.wav_addr.is_none());
}
#[test]
fn compile_program_vwl_and_continue_round_trip() {
let vs = 0x1000u32;
let ve = 0x2000u32;
let fmt = 0x3000u32;
let bytecode = compile_program(&[
ProgInstn::VwlStart { addr: vs },
ProgInstn::VwlEnding { addr: ve },
ProgInstn::Continue,
ProgInstn::Fmt { addr: fmt, param: 7 },
ProgInstn::Return,
]);
let phonindex = pad(&bytecode);
let r = scan_phoneme(PROG, &phonindex);
assert_eq!(r.vwlstart_addr, Some(vs), "VWLSTART did not round-trip");
assert_eq!(r.vwlending_addr, Some(ve), "VWLENDING did not round-trip");
assert_eq!(r.fmt_addr, Some(fmt), "FMT after CONTINUE was not reached");
assert_eq!(r.fmt_param, 7);
}
#[test]
fn compile_program_callph_round_trip() {
let sub_off = 4u16;
let addr = 0x2000u32;
let main = compile_program(&[ProgInstn::CallPh { program: sub_off }, ProgInstn::Return]);
assert_eq!(main.len(), 6, "CALLPH+RETURN should be 3 words"); let sub = compile_program(&[ProgInstn::Fmt { addr, param: 9 }, ProgInstn::Return]);
let mut bytes = main;
bytes.extend_from_slice(&sub);
let phonindex = pad(&bytes);
let r = scan_phoneme(PROG, &phonindex);
assert_eq!(r.fmt_addr, Some(addr), "CALLPH did not resolve the callee's FMT");
assert_eq!(r.fmt_param, 9);
}
const PROG: u16 = 1;
fn pad(insns: &[u8]) -> Vec<u8> {
let mut v = vec![0u8, 0u8]; v.extend_from_slice(insns);
v
}
fn make_fmt_phonindex(fmt_addr: u32) -> Vec<u8> {
let instn: u16 = 0xb000 | (((fmt_addr >> 18) & 0xf) as u16);
let next: u16 = ((fmt_addr >> 2) & 0xffff) as u16;
let mut insns = vec![0u8; 4];
insns[0..2].copy_from_slice(&instn.to_le_bytes());
insns[2..4].copy_from_slice(&next.to_le_bytes());
pad(&insns)
}
fn make_ipa_then_fmt(fmt_addr: u32) -> Vec<u8> {
let ipa_instn: u16 = ((I_IPA_NAME as u16) << 8) | 2; let ipa_data: u16 = u16::from_be_bytes([b'e', b':']);
let fmt_instn: u16 = 0xb000 | (((fmt_addr >> 18) & 0xf) as u16);
let fmt_next: u16 = ((fmt_addr >> 2) & 0xffff) as u16;
let mut insns = vec![0u8; 8];
insns[0..2].copy_from_slice(&ipa_instn.to_le_bytes());
insns[2..4].copy_from_slice(&ipa_data.to_le_bytes());
insns[4..6].copy_from_slice(&fmt_instn.to_le_bytes());
insns[6..8].copy_from_slice(&fmt_next.to_le_bytes());
pad(&insns)
}
#[test]
fn scan_simple_fmt() {
let addr = 0x1234u32 * 4;
let phonindex = make_fmt_phonindex(addr);
let result = scan_phoneme(PROG, &phonindex);
assert_eq!(result.fmt_addr, Some(addr));
assert!(result.wav_addr.is_none());
}
#[test]
fn scan_extracts_vowel_transition() {
let words: [u16; 5] = [0xa10a, 0x1234, 0x1234, 0x5678, INSTN_RETURN];
let mut insns = Vec::new();
for w in &words {
insns.extend_from_slice(&w.to_le_bytes());
}
let r = scan_phoneme(PROG, &pad(&insns));
assert_eq!(r.vowel_transition[0], 0x0a_1234);
assert_eq!(r.vowel_transition[1], 0x1234_5678);
assert_eq!(r.vowel_transition[2], 0); assert_eq!(r.vowel_transition[3], 0);
}
#[test]
fn scan_ipa_then_fmt() {
let addr = 0x5678u32 * 4;
let phonindex = make_ipa_then_fmt(addr);
let result = scan_phoneme(PROG, &phonindex);
assert_eq!(result.fmt_addr, Some(addr));
}
#[test]
fn scan_zero_program_returns_empty() {
let phonindex = vec![0u8; 4];
let result = scan_phoneme(0, &phonindex); assert!(result.fmt_addr.is_none());
assert!(result.wav_addr.is_none());
}
#[test]
fn scan_return_stops_early() {
let addr = 0x1000u32 * 4;
let fmt_instn: u16 = 0xb000 | (((addr >> 18) & 0xf) as u16);
let fmt_next: u16 = ((addr >> 2) & 0xffff) as u16;
let mut insns = vec![0u8; 8];
insns[0..2].copy_from_slice(&INSTN_RETURN.to_le_bytes());
insns[2..4].copy_from_slice(&[0, 0]);
insns[4..6].copy_from_slice(&fmt_instn.to_le_bytes());
insns[6..8].copy_from_slice(&fmt_next.to_le_bytes());
let result = scan_phoneme(PROG, &pad(&insns));
assert!(result.fmt_addr.is_none(), "RETURN should stop scanner");
}
#[test]
fn num_instn_words_fmt() {
assert_eq!(num_instn_words(0xb000), 2);
assert_eq!(num_instn_words(0xb123), 2);
}
#[test]
fn num_instn_words_vowelin() {
assert_eq!(num_instn_words(0xa100), 4);
assert_eq!(num_instn_words(0xa200), 4);
}
#[test]
fn num_instn_words_ipa_name_4bytes() {
let instn: u16 = ((I_IPA_NAME as u16) << 8) | 4;
assert_eq!(num_instn_words(instn), 3);
}
#[test]
fn num_instn_words_callph() {
assert_eq!(num_instn_words(0x9100), 2);
assert_eq!(num_instn_words(0x9200), 2);
assert_eq!(num_instn_words(0x9300), 2);
}
#[test]
fn scan_wav_only() {
let addr = 0x2000u32 * 4;
let wav_instn: u16 = 0xc000 | (((addr >> 18) & 0xf) as u16);
let wav_next: u16 = ((addr >> 2) & 0xffff) as u16;
let mut insns = vec![0u8; 4];
insns[0..2].copy_from_slice(&wav_instn.to_le_bytes());
insns[2..4].copy_from_slice(&wav_next.to_le_bytes());
let result = scan_phoneme(PROG, &pad(&insns));
assert!(result.fmt_addr.is_none());
assert_eq!(result.wav_addr, Some(addr));
}
#[test]
fn scan_wavadd_after_fmt() {
let fmt_a = 0x1000u32 * 4;
let wav_a = 0x2000u32 * 4;
let fmt_instn: u16 = 0xb000 | (((fmt_a >> 18) & 0xf) as u16);
let fmt_next: u16 = ((fmt_a >> 2) & 0xffff) as u16;
let add_instn: u16 = 0xf000 | (((wav_a >> 18) & 0xf) as u16);
let add_next: u16 = ((wav_a >> 2) & 0xffff) as u16;
let mut insns = vec![0u8; 8];
insns[0..2].copy_from_slice(&fmt_instn.to_le_bytes());
insns[2..4].copy_from_slice(&fmt_next.to_le_bytes());
insns[4..6].copy_from_slice(&add_instn.to_le_bytes());
insns[6..8].copy_from_slice(&add_next.to_le_bytes());
let result = scan_phoneme(PROG, &pad(&insns));
assert_eq!(result.fmt_addr, Some(fmt_a));
assert_eq!(result.wav_addr, Some(wav_a));
}
fn mk_ph(typ: u8, mnem: &str) -> PhonemeTab {
PhonemeTab {
typ,
mnemonic: PhonemeTab::pack_mnemonic(mnem),
..Default::default()
}
}
fn words_bytes(words: &[u16]) -> Vec<u8> {
let mut v = vec![0u8, 0u8]; for w in words {
v.extend_from_slice(&w.to_le_bytes());
}
v
}
#[test]
fn interpret_matches_scan_for_plain_fmt() {
let addr = 0x1234u32 * 4;
let phonindex = make_fmt_phonindex(addr);
let nb = Neighbours::default();
let r = interpret_phoneme(PROG, &phonindex, &nb, |_| None);
assert_eq!(r.fmt_addr, Some(addr));
}
#[test]
fn interpret_picks_branch_by_next_vowel() {
let a = 0x1000u32 * 4;
let b = 0x2000u32 * 4;
let cond = 0x298a; let jf = JUMP_FALSE | 3; let fmt_a_i = 0xb000 | (((a >> 18) & 0xf) as u16);
let fmt_a_n = ((a >> 2) & 0xffff) as u16;
let fmt_b_i = 0xb000 | (((b >> 18) & 0xf) as u16);
let fmt_b_n = ((b >> 2) & 0xffff) as u16;
let phonindex = words_bytes(&[cond, jf, fmt_a_i, fmt_a_n, fmt_b_i, fmt_b_n]);
let lookup = |c: u8| -> Option<PhonemeTab> {
match c {
20 => Some(mk_ph(PH_VOWEL, "a")),
30 => Some(mk_ph(4, "t")), _ => None,
}
};
let nb_v = Neighbours { this: 10, next: 20, ..Default::default() };
let r = interpret_phoneme(PROG, &phonindex, &nb_v, &lookup);
assert_eq!(r.fmt_addr, Some(b), "next=vowel should reach default FMT(B)");
let nb_c = Neighbours { this: 10, next: 30, ..Default::default() };
let r = interpret_phoneme(PROG, &phonindex, &nb_c, &lookup);
assert_eq!(r.fmt_addr, Some(a), "next=consonant should reach FMT(A)");
}
#[test]
fn interpret_prevph_code_match() {
let a = 0x1000u32 * 4;
let b = 0x2000u32 * 4;
let cond = 0x2000 | 30; let jf = JUMP_FALSE | 3;
let fmt_a_i = 0xb000 | (((a >> 18) & 0xf) as u16);
let fmt_a_n = ((a >> 2) & 0xffff) as u16;
let fmt_b_i = 0xb000 | (((b >> 18) & 0xf) as u16);
let fmt_b_n = ((b >> 2) & 0xffff) as u16;
let phonindex = words_bytes(&[cond, jf, fmt_a_i, fmt_a_n, fmt_b_i, fmt_b_n]);
let lookup = |c: u8| -> Option<PhonemeTab> {
match c {
30 => Some(mk_ph(4, "t")),
31 => Some(mk_ph(4, "p")),
_ => None,
}
};
let nb_t = Neighbours { this: 10, prev: 30, ..Default::default() };
let r = interpret_phoneme(PROG, &phonindex, &nb_t, &lookup);
assert_eq!(r.fmt_addr, Some(a), "prev=t should reach FMT(A)");
let nb_p = Neighbours { this: 10, prev: 31, ..Default::default() };
let r = interpret_phoneme(PROG, &phonindex, &nb_p, &lookup);
assert_eq!(r.fmt_addr, Some(b), "prev=p should reach default FMT(B)");
}
#[test]
fn interpret_callph_returns_and_continues() {
let main_addr = 0x3000u32 * 4;
let sub_addr = 0x4000u32 * 4;
let callph_i = 0x9100u16; let callph_d = 6u16; let fmt_m_i = 0xb000 | (((main_addr >> 18) & 0xf) as u16);
let fmt_m_n = ((main_addr >> 2) & 0xffff) as u16;
let fmt_s_i = 0xb000 | (((sub_addr >> 18) & 0xf) as u16);
let fmt_s_n = ((sub_addr >> 2) & 0xffff) as u16;
let phonindex = words_bytes(&[
callph_i, callph_d, fmt_m_i, fmt_m_n, INSTN_RETURN, fmt_s_i, fmt_s_n,
]);
let nb = Neighbours { this: 10, ..Default::default() };
let r = interpret_phoneme(PROG, &phonindex, &nb, |_| None);
assert_eq!(r.fmt_addr, Some(main_addr), "main FMT should win after CALLPH");
}
}