use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum DebugRecordKind {
CallSite = 0,
SourceSpan = 1,
LineNumber = 2,
VariableName = 3,
TypeAnnotation = 4,
AssertionContext = 5,
BreakpointCandidate = 6,
GenericInstantiation = 7,
IfcLabelAnnotation = 8,
WcetMarker = 9,
OptimisationMarker = 10,
VerifierWitness = 11,
}
impl DebugRecordKind {
pub fn as_u8(self) -> u8 {
self as u8
}
pub fn from_u8(byte: u8) -> Option<Self> {
Some(match byte {
0 => Self::CallSite,
1 => Self::SourceSpan,
2 => Self::LineNumber,
3 => Self::VariableName,
4 => Self::TypeAnnotation,
5 => Self::AssertionContext,
6 => Self::BreakpointCandidate,
7 => Self::GenericInstantiation,
8 => Self::IfcLabelAnnotation,
9 => Self::WcetMarker,
10 => Self::OptimisationMarker,
11 => Self::VerifierWitness,
_ => return None,
})
}
}
pub type Span = (u16, u32, u32);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebugRecord {
pub op_index: u32,
pub kind: DebugRecordKind,
pub operands: Vec<u16>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DebugPool {
pub string_pool: Vec<String>,
pub span_pool: Vec<Span>,
pub type_pool: Vec<Vec<u8>>,
pub records: Vec<DebugRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation<'a> {
pub file: Option<&'a str>,
pub byte_offset: u32,
pub byte_length: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DebugMetaError {
Truncated,
UnknownRecordKind(u8),
InvalidUtf8,
}
fn put_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_le_bytes());
}
fn put_u32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_le_bytes());
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Reader { bytes, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8], DebugMetaError> {
let end = self.pos.checked_add(n).ok_or(DebugMetaError::Truncated)?;
let slice = self
.bytes
.get(self.pos..end)
.ok_or(DebugMetaError::Truncated)?;
self.pos = end;
Ok(slice)
}
fn u8(&mut self) -> Result<u8, DebugMetaError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, DebugMetaError> {
let b = self.take(2)?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
fn u32(&mut self) -> Result<u32, DebugMetaError> {
let b = self.take(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
}
impl DebugPool {
pub fn is_empty(&self) -> bool {
self.string_pool.is_empty()
&& self.span_pool.is_empty()
&& self.type_pool.is_empty()
&& self.records.is_empty()
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
put_u32(&mut out, self.string_pool.len() as u32);
for s in &self.string_pool {
put_u32(&mut out, s.len() as u32);
out.extend_from_slice(s.as_bytes());
}
put_u32(&mut out, self.span_pool.len() as u32);
for &(file_idx, offset, length) in &self.span_pool {
put_u16(&mut out, file_idx);
put_u32(&mut out, offset);
put_u32(&mut out, length);
}
put_u32(&mut out, self.type_pool.len() as u32);
for blob in &self.type_pool {
put_u32(&mut out, blob.len() as u32);
out.extend_from_slice(blob);
}
let mut records: Vec<&DebugRecord> = self.records.iter().collect();
records.sort_by(|a, b| {
a.op_index
.cmp(&b.op_index)
.then(a.kind.as_u8().cmp(&b.kind.as_u8()))
.then(a.operands.cmp(&b.operands))
});
put_u32(&mut out, records.len() as u32);
for record in records {
put_u32(&mut out, record.op_index);
out.push(record.kind.as_u8());
put_u16(&mut out, record.operands.len() as u16);
for &operand in &record.operands {
put_u16(&mut out, operand);
}
}
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, DebugMetaError> {
let mut r = Reader::new(bytes);
let string_count = r.u32()? as usize;
let mut string_pool = Vec::with_capacity(string_count);
for _ in 0..string_count {
let len = r.u32()? as usize;
let raw = r.take(len)?;
let s = core::str::from_utf8(raw).map_err(|_| DebugMetaError::InvalidUtf8)?;
string_pool.push(String::from(s));
}
let span_count = r.u32()? as usize;
let mut span_pool = Vec::with_capacity(span_count);
for _ in 0..span_count {
let file_idx = r.u16()?;
let offset = r.u32()?;
let length = r.u32()?;
span_pool.push((file_idx, offset, length));
}
let type_count = r.u32()? as usize;
let mut type_pool = Vec::with_capacity(type_count);
for _ in 0..type_count {
let len = r.u32()? as usize;
type_pool.push(r.take(len)?.to_vec());
}
let record_count = r.u32()? as usize;
let mut records = Vec::with_capacity(record_count);
for _ in 0..record_count {
let op_index = r.u32()?;
let kind_byte = r.u8()?;
let kind = DebugRecordKind::from_u8(kind_byte)
.ok_or(DebugMetaError::UnknownRecordKind(kind_byte))?;
let operand_count = r.u16()? as usize;
let mut operands = Vec::with_capacity(operand_count);
for _ in 0..operand_count {
operands.push(r.u16()?);
}
records.push(DebugRecord {
op_index,
kind,
operands,
});
}
Ok(DebugPool {
string_pool,
span_pool,
type_pool,
records,
})
}
pub fn records_at(&self, op_index: u32) -> impl Iterator<Item = &DebugRecord> {
self.records.iter().filter(move |r| r.op_index == op_index)
}
pub fn string(&self, index: u16) -> Option<&str> {
self.string_pool.get(index as usize).map(|s| s.as_str())
}
pub fn span(&self, index: u16) -> Option<Span> {
self.span_pool.get(index as usize).copied()
}
pub fn type_blob(&self, index: u16) -> Option<&[u8]> {
self.type_pool.get(index as usize).map(|v| v.as_slice())
}
pub fn source_location(&self, record: &DebugRecord) -> Option<SourceLocation<'_>> {
match record.kind {
DebugRecordKind::CallSite
| DebugRecordKind::SourceSpan
| DebugRecordKind::AssertionContext
| DebugRecordKind::BreakpointCandidate => {}
_ => return None,
}
let span_idx = *record.operands.first()?;
let (file_idx, byte_offset, byte_length) = self.span(span_idx)?;
Some(SourceLocation {
file: self.string(file_idx),
byte_offset,
byte_length,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
fn sample_pool() -> DebugPool {
DebugPool {
string_pool: vec!["main.kel".to_string(), "count".to_string()],
span_pool: vec![(0, 10, 4), (0, 20, 5)],
type_pool: vec![vec![0x01, 0x02], vec![0x03]],
records: vec![
DebugRecord {
op_index: 7,
kind: DebugRecordKind::CallSite,
operands: vec![0, 0],
},
DebugRecord {
op_index: 3,
kind: DebugRecordKind::VariableName,
operands: vec![1],
},
],
}
}
#[test]
fn records_at_returns_records_for_a_position() {
let pool = sample_pool();
let at7: alloc::vec::Vec<_> = pool.records_at(7).collect();
assert_eq!(at7.len(), 1);
assert_eq!(at7[0].kind, DebugRecordKind::CallSite);
assert!(pool.records_at(99).next().is_none());
}
#[test]
fn source_location_resolves_call_site_span() {
let pool = sample_pool();
let rec = pool.records_at(7).next().unwrap();
let loc = pool.source_location(rec).expect("call site resolves");
assert_eq!(loc.file, Some("main.kel"));
assert_eq!(loc.byte_offset, 10);
assert_eq!(loc.byte_length, 4);
}
#[test]
fn source_location_is_none_for_non_span_kinds() {
let pool = sample_pool();
let var = pool.records_at(3).next().unwrap();
assert_eq!(var.kind, DebugRecordKind::VariableName);
assert!(pool.source_location(var).is_none());
}
#[test]
fn source_location_is_none_on_dangling_span_index() {
let mut pool = DebugPool::default();
pool.records.push(DebugRecord {
op_index: 0,
kind: DebugRecordKind::CallSite,
operands: vec![5], });
let rec = &pool.records[0];
assert!(pool.source_location(rec).is_none());
}
#[test]
fn all_kinds_round_trip_through_u8() {
let kinds = [
DebugRecordKind::CallSite,
DebugRecordKind::SourceSpan,
DebugRecordKind::LineNumber,
DebugRecordKind::VariableName,
DebugRecordKind::TypeAnnotation,
DebugRecordKind::AssertionContext,
DebugRecordKind::BreakpointCandidate,
DebugRecordKind::GenericInstantiation,
DebugRecordKind::IfcLabelAnnotation,
DebugRecordKind::WcetMarker,
DebugRecordKind::OptimisationMarker,
DebugRecordKind::VerifierWitness,
];
for k in kinds {
assert_eq!(DebugRecordKind::from_u8(k.as_u8()), Some(k));
}
assert_eq!(DebugRecordKind::from_u8(12), None);
}
#[test]
fn empty_pool_round_trips() {
let pool = DebugPool::default();
assert!(pool.is_empty());
let bytes = pool.encode();
let decoded = DebugPool::decode(&bytes).expect("decode");
assert_eq!(decoded, pool);
}
#[test]
fn populated_pool_round_trips() {
let pool = sample_pool();
let decoded = DebugPool::decode(&pool.encode()).expect("decode");
assert_eq!(decoded.string_pool, pool.string_pool);
assert_eq!(decoded.span_pool, pool.span_pool);
assert_eq!(decoded.type_pool, pool.type_pool);
assert_eq!(decoded.records.len(), 2);
assert_eq!(decoded.records[0].op_index, 3);
assert_eq!(decoded.records[1].op_index, 7);
}
#[test]
fn encode_is_deterministic_regardless_of_record_order() {
let pool_a = sample_pool();
let mut pool_b = sample_pool();
pool_b.records.reverse();
assert_eq!(
pool_a.encode(),
pool_b.encode(),
"record insertion order must not affect encoded bytes"
);
}
#[test]
fn decode_then_encode_is_byte_identical() {
let bytes = sample_pool().encode();
let reencoded = DebugPool::decode(&bytes).expect("decode").encode();
assert_eq!(bytes, reencoded);
}
#[test]
fn decode_rejects_truncated_input() {
let bytes = sample_pool().encode();
let err = DebugPool::decode(&bytes[..bytes.len() - 1]).unwrap_err();
assert_eq!(err, DebugMetaError::Truncated);
}
#[test]
fn decode_rejects_unknown_record_kind() {
let mut pool = DebugPool::default();
pool.records.push(DebugRecord {
op_index: 0,
kind: DebugRecordKind::CallSite,
operands: vec![],
});
let mut bytes = pool.encode();
bytes[20] = 200;
let err = DebugPool::decode(&bytes).unwrap_err();
assert_eq!(err, DebugMetaError::UnknownRecordKind(200));
}
#[test]
fn decode_rejects_non_utf8_string() {
let mut bytes = Vec::new();
put_u32(&mut bytes, 1); put_u32(&mut bytes, 1); bytes.push(0xFF); put_u32(&mut bytes, 0); put_u32(&mut bytes, 0); put_u32(&mut bytes, 0); let err = DebugPool::decode(&bytes).unwrap_err();
assert_eq!(err, DebugMetaError::InvalidUtf8);
}
}