use celox_design::InitialStateWriteRun;
use num_bigint::BigUint;
#[derive(Debug, Clone)]
pub struct MemoryFileError {
pub message: String,
pub at_destination: bool,
}
impl MemoryFileError {
fn syntax(message: impl Into<String>) -> Self {
Self {
message: message.into(),
at_destination: false,
}
}
fn destination(message: impl Into<String>) -> Self {
Self {
message: message.into(),
at_destination: true,
}
}
}
pub struct ParsedMemoryWrites {
pub runs: Vec<InitialStateWriteRun>,
pub words: usize,
}
pub struct RangedMemoryWrites {
pub writes: ParsedMemoryWrites,
pub warning: Option<String>,
}
pub fn parse_memory_range(
content: &str,
radix: u32,
width: usize,
start: i128,
finish: i128,
) -> Result<RangedMemoryWrites, MemoryFileError> {
let low = start.min(finish);
let high = start.max(finish);
let step = if start <= finish { 1 } else { -1 };
let mut address = Some(start);
let mut runs = Vec::new();
let mut words = 0usize;
let mut directives = false;
for token in memory_tokens(content) {
if let Some(text) = token.strip_prefix('@') {
directives = true;
let index = i128::from_str_radix(text, 16).map_err(|error| {
MemoryFileError::syntax(format!("invalid address directive {token}: {error}"))
})?;
if index < low || index > high {
return Err(MemoryFileError::destination(format!(
"address {index} is outside requested range [{start}:{finish}]"
)));
}
address = Some(index);
continue;
}
let (value, mask) = parse_memory_word(&token, radix, width)?;
words += 1;
if let Some(index) = address.filter(|index| *index >= low && *index <= high) {
let offset = usize::try_from(index.abs_diff(low))
.ok()
.and_then(|index| index.checked_mul(width))
.ok_or_else(|| MemoryFileError::destination("memory address offset overflows"))?;
runs.push(InitialStateWriteRun {
bit_offset: offset,
bit_width: width,
value_bytes: biguint_to_fixed_le_bytes(&value, width),
mask_bytes: biguint_to_fixed_le_bytes(&mask, width),
});
address = index.checked_add(step);
}
}
let expected = start.abs_diff(finish).checked_add(1);
let warning = (!directives && expected != Some(words as u128)).then(|| format!(
"file contains {words} words for requested range [{start}:{finish}] ({} words); excess words are ignored and missing words leave memory unchanged",
expected.map_or_else(|| "too many".to_string(), |count| count.to_string()),
));
Ok(RangedMemoryWrites {
writes: ParsedMemoryWrites { runs, words },
warning,
})
}
pub fn parse_memory_write_runs(
content: &str,
radix: u32,
width: usize,
start_addr: usize,
depth: usize,
) -> Result<ParsedMemoryWrites, MemoryFileError> {
let mut runs: Vec<InitialStateWriteRun> = Vec::new();
let mut addr = 0usize;
let mut words = 0usize;
for word_token in memory_tokens(content) {
if let Some(address) = word_token.strip_prefix('@') {
addr = usize::from_str_radix(address, 16).map_err(|err| {
MemoryFileError::syntax(format!("invalid address directive {word_token}: {err}"))
})?;
continue;
}
let (value, mask) = parse_memory_word(&word_token, radix, width)?;
let Some(dst_addr) = start_addr.checked_add(addr) else {
return Err(MemoryFileError::destination(
"address exceeds destination depth",
));
};
if dst_addr >= depth {
return Err(MemoryFileError::destination(format!(
"address {dst_addr} exceeds destination depth {depth}"
)));
}
let bit_offset = dst_addr * width;
let value_bytes = biguint_to_fixed_le_bytes(&value, width);
let mask_bytes = biguint_to_fixed_le_bytes(&mask, width);
if let Some(last) = runs.last_mut()
&& last.bit_offset + last.bit_width == bit_offset
&& last.bit_offset % 8 == 0
&& last.bit_width % 8 == 0
&& width.is_multiple_of(8)
{
last.bit_width += width;
last.value_bytes.extend(value_bytes);
last.mask_bytes.extend(mask_bytes);
} else {
runs.push(InitialStateWriteRun {
bit_offset,
bit_width: width,
value_bytes,
mask_bytes,
});
}
words += 1;
addr = addr
.checked_add(1)
.ok_or_else(|| MemoryFileError::destination("address exceeds destination depth"))?;
}
Ok(ParsedMemoryWrites { runs, words })
}
pub fn biguint_to_fixed_le_bytes(value: &BigUint, width: usize) -> Vec<u8> {
let byte_len = width.div_ceil(8);
let mut out = vec![0; byte_len];
let src = value.to_bytes_le();
let copy_len = src.len().min(byte_len);
out[..copy_len].copy_from_slice(&src[..copy_len]);
if !width.is_multiple_of(8) && !out.is_empty() {
let keep = (1u8 << (width % 8)) - 1;
*out.last_mut().unwrap() &= keep;
}
out
}
fn memory_tokens(content: &str) -> Vec<String> {
let mut out = String::with_capacity(content.len());
let bytes = content.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'/' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'/' => {
i += 2;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
out.push(' ');
continue;
}
b'*' => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i = (i + 2).min(bytes.len());
out.push(' ');
continue;
}
_ => {}
}
}
out.push(bytes[i] as char);
i += 1;
}
out.split_whitespace()
.map(|token| token.replace('_', ""))
.filter(|token| !token.is_empty())
.collect()
}
fn parse_memory_word(
token: &str,
radix: u32,
width: usize,
) -> Result<(BigUint, BigUint), MemoryFileError> {
let bits_per_digit = match radix {
2 => 1,
16 => 4,
_ => {
return Err(MemoryFileError::syntax(format!(
"unsupported memory file radix {radix}"
)));
}
};
let mut value = BigUint::default();
let mut mask = BigUint::default();
for ch in token.chars() {
value <<= bits_per_digit;
mask <<= bits_per_digit;
match ch {
'0'..='9' | 'a'..='f' | 'A'..='F' => {
let Some(digit) = ch.to_digit(radix) else {
return Err(invalid_memory_word(token));
};
value |= BigUint::from(digit);
}
'x' | 'X' | '?' => {
mask |= (BigUint::from(1u8) << bits_per_digit) - BigUint::from(1u8);
}
'z' | 'Z' => {
let unknown = (BigUint::from(1u8) << bits_per_digit) - BigUint::from(1u8);
value |= &unknown;
mask |= unknown;
}
_ => return Err(invalid_memory_word(token)),
}
}
if width == 0 {
return Ok((BigUint::default(), BigUint::default()));
}
let keep = (BigUint::from(1u8) << width) - BigUint::from(1u8);
Ok((value & &keep, mask & keep))
}
fn invalid_memory_word(token: &str) -> MemoryFileError {
MemoryFileError::syntax(format!("invalid data token {token}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ranged_loads_follow_direction_and_absolute_directives() {
for (start, finish, content, expected) in [
(3, 1, "a b c", vec![(16, 10), (8, 11), (0, 12)]),
(1, 3, "a @2 b c", vec![(0, 10), (8, 11), (16, 12)]),
(3, 1, "@2 b c", vec![(8, 11), (0, 12)]),
(-2, 0, "a b c", vec![(0, 10), (8, 11), (16, 12)]),
] {
let parsed = parse_memory_range(content, 16, 8, start, finish).unwrap();
assert!(parsed.warning.is_none());
let actual = parsed
.writes
.runs
.iter()
.map(|run| (run.bit_offset, run.value_bytes[0]))
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
}
#[test]
fn mismatched_counts_warn_and_preserve_only_available_range_words() {
for (content, loaded) in [("a", 1), ("a b c d", 2)] {
let parsed = parse_memory_range(content, 16, 8, 3, 2).unwrap();
assert!(
parsed
.warning
.as_deref()
.unwrap()
.contains("requested range [3:2]")
);
assert_eq!(parsed.writes.runs.len(), loaded);
}
for content in ["@4", "@1 a", "a b @4"] {
let error = parse_memory_range(content, 16, 8, 3, 2).err().unwrap();
assert!(error.at_destination);
assert!(error.message.contains("requested range [3:2]"));
assert!(!error.message.contains("depth"));
}
assert!(parse_memory_range("g", 16, 8, 3, 2).is_err());
assert!(parse_memory_range("2", 2, 8, 3, 2).is_err());
}
}