use crate::grain::pos::Site;
use crate::Position;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
#[derive(Debug, Clone, Default)]
pub enum Positions {
#[default]
Stripped,
Dense(Box<[Position]>),
}
impl Positions {
pub(crate) fn dense(positions: Vec<Position>) -> Self {
if positions.iter().all(|pos| pos.is_none()) {
return Self::Stripped;
}
Self::Dense(positions.into_boxed_slice())
}
#[must_use]
pub fn get(&self, pc: usize) -> Position {
match self {
Self::Stripped => Position::NONE,
Self::Dense(positions) => positions.get(pc).copied().unwrap_or(Position::NONE),
}
}
#[must_use]
pub fn is_stripped(&self) -> bool {
matches!(self, Self::Stripped)
}
#[must_use]
pub fn to_table(&self) -> Vec<u8> {
let Self::Dense(positions) = self else {
return crate::grain::pos::encode(core::iter::empty());
};
crate::grain::pos::encode(positions.iter().enumerate().filter_map(|(pc, pos)| {
let line = pos.line()?;
Some((
pc as u32,
Site {
line: line as u32,
column: pos.position().unwrap_or(0) as u32,
},
))
}))
}
pub fn from_table(table: &[u8], code: &[u8]) -> Result<Self, TableError> {
crate::grain::pos::check(table).map_err(TableError::Malformed)?;
let count = crate::grain::pos::count(table).map_err(TableError::Malformed)?;
if count == 0 {
return Ok(Self::Stripped);
}
let mut positions = vec![Position::NONE; code.len()];
let mut matched = 0usize;
let mut at = 0usize;
while at < code.len() {
if let Some(site) = crate::grain::pos::resolve(table, at as u32) {
positions[at] = site_to_position(site);
matched += 1;
}
match crate::grain::bytecode::code::width(code, at) {
Some(width) => at += width,
None => break,
}
}
if matched != count as usize {
return Err(TableError::PastTheEnd {
entries: count as usize,
matched,
instructions: code.len(),
});
}
Ok(Self::dense(positions))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TableError {
Malformed(crate::grain::pos::Error),
PastTheEnd {
entries: usize,
matched: usize,
instructions: usize,
},
WrongProgram {
expected: u128,
found: u128,
},
ChainStream(super::sites::StreamError),
ChainCount {
sites: usize,
slots: usize,
},
}
impl core::fmt::Display for TableError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Malformed(err) => write!(f, "position table is malformed: {err:?}"),
Self::PastTheEnd {
entries,
matched,
instructions,
} => write!(
f,
"position table has {entries} entries but only {matched} begin an instruction \
of this chunk's {instructions} code bytes, so it is a different program's"
),
Self::WrongProgram { expected, found } => write!(
f,
"sidecar belongs to debug id {expected:#034x}, not to this program's {found:#034x}"
),
Self::ChainStream(err) => write!(f, "{err}"),
Self::ChainCount { sites, slots } => write!(
f,
"chain site stream holds {sites} sites but this program's chains carry {slots} \
positions, so it is a different program's"
),
}
}
}
pub(crate) fn site_to_position(site: Site) -> Position {
let (Ok(line), Ok(column)) = (u16::try_from(site.line), u16::try_from(site.column)) else {
return Position::NONE;
};
if line == 0 {
return Position::NONE;
}
Position::new(line, column)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grain::bytecode::{assemble, Op};
fn code() -> Vec<u8> {
let (code, _) = assemble(&[Op::Unit, Op::Unit, Op::Unit, Op::Unit]).expect("must assemble");
assert_eq!(code.len(), 4, "the sample assumes one byte an instruction");
code
}
fn sample() -> Positions {
Positions::dense(vec![
Position::NONE,
Position::new(1, 5),
Position::NONE,
Position::new(3, 0),
])
}
#[test]
fn a_table_survives_the_round_trip() {
let table = sample().to_table();
let back = Positions::from_table(&table, &code()).expect("the table is this chunk's");
for pc in 0..4 {
assert_eq!(back.get(pc), sample().get(pc), "at {pc}");
}
}
#[test]
fn the_start_of_a_line_survives() {
let table = sample().to_table();
let back = Positions::from_table(&table, &code()).unwrap();
assert_eq!(back.get(3), Position::new(3, 0));
}
#[test]
fn stripping_is_what_a_missing_table_reads_as() {
let stripped = Positions::Stripped;
assert!(stripped.is_stripped());
assert_eq!(stripped.get(0), Position::NONE);
let empty = Positions::from_table(&stripped.to_table(), &code()).unwrap();
assert!(empty.is_stripped());
}
#[test]
fn positions_that_are_all_absent_collapse() {
assert!(Positions::dense(vec![Position::NONE; 8]).is_stripped());
}
#[test]
#[cfg(not(feature = "no_position"))]
fn a_table_from_another_program_is_refused() {
let table = sample().to_table();
let (short, _) = assemble(&[Op::Unit, Op::Unit]).expect("must assemble");
assert!(matches!(
Positions::from_table(&table, &short),
Err(TableError::PastTheEnd { .. }),
));
}
#[test]
#[cfg(not(feature = "no_position"))]
fn an_address_that_does_not_begin_an_instruction_is_refused() {
let table = sample().to_table();
let (wide, _) = assemble(&[Op::Const(0), Op::Unit]).expect("must assemble");
assert_eq!(wide.len(), 4, "the case needs the same four bytes");
assert!(matches!(
Positions::from_table(&table, &wide),
Err(TableError::PastTheEnd { .. }),
));
}
#[test]
fn a_malformed_table_is_refused_rather_than_half_applied() {
let table = sample().to_table();
assert!(matches!(
Positions::from_table(&table[..table.len() - 1], &code()),
Err(TableError::Malformed(..)),
));
}
#[test]
fn an_address_past_the_end_reads_as_no_position() {
assert_eq!(sample().get(9999), Position::NONE);
}
}