Skip to main content

celox_runtime/
state_file.rs

1//! Layout-independent state files.
2//!
3//! A state file records the value of every state object by its hierarchical
4//! path instead of by memory offset. It can therefore be loaded into a
5//! simulator built with a different backend, optimization level or memory
6//! layout, as long as the design declares the same state.
7//!
8//! The encoding is little-endian binary:
9//!
10//! ```text
11//! magic "CXSTATE\0" | version u16 | flags u16
12//! object count u32, then per object:
13//!   path str | width u64 | role u8 | is_4state u8
14//!   value [ceil(width/8) bytes] | mask [same size, only for four-state objects
15//!   in a four-state file]
16//! schedule (only with FLAG_SCHEDULE):
17//!   time u64
18//!   clock count u32, then per clock: event str | period u64
19//!   event count u32, then per event: scheduled event
20//!   periodic count u32, then per marker: scheduled event | count u64
21//!   high count u32, then per event: event str
22//! scheduled event: time u64 | event str | signal str | value u8
23//! str: byte length u32 | UTF-8
24//! ```
25
26use std::io::{self, Read, Write};
27
28const MAGIC: &[u8; 8] = b"CXSTATE\0";
29const VERSION: u16 = 1;
30const FLAG_FOUR_STATE: u16 = 1 << 0;
31const FLAG_SCHEDULE: u16 = 1 << 1;
32
33/// How a state object gets its value.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum StateRole {
36    /// Held across evaluations: registers, memories and inputs.
37    State,
38    /// Written by combinational logic and recomputed from the other state.
39    Comb,
40}
41
42/// The saved value of one state object.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct StateObject {
45    pub path: String,
46    pub width: usize,
47    pub role: StateRole,
48    pub is_4state: bool,
49    /// `ceil(width / 8)` little-endian bytes.
50    pub value: Vec<u8>,
51    /// X/Z mask with the layout of `value`, present for four-state objects
52    /// of a four-state simulation.
53    pub mask: Option<Vec<u8>>,
54}
55
56/// A pending event of a time-based simulation, identified by name.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ScheduledEvent {
59    pub time: u64,
60    /// Path of the event (clock or reset) domain.
61    pub event: String,
62    /// Path of the signal the event drives.
63    pub signal: String,
64    pub value: u8,
65}
66
67/// Scheduling state of a time-based simulation.
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ScheduleRecord {
70    pub time: u64,
71    /// Periodic clocks as (event path, period).
72    pub clocks: Vec<(String, u64)>,
73    /// Pending events, in no particular order.
74    pub events: Vec<ScheduledEvent>,
75    /// Pending events that a periodic clock re-schedules when they fire, with
76    /// the number of identical such events.
77    pub periodic: Vec<(ScheduledEvent, u64)>,
78    /// Event paths whose signal was high when edge detection last sampled it.
79    pub high_events: Vec<String>,
80}
81
82/// Contents of a state file.
83#[derive(Debug, Clone, PartialEq, Eq, Default)]
84pub struct StateFile {
85    pub four_state: bool,
86    pub objects: Vec<StateObject>,
87    pub schedule: Option<ScheduleRecord>,
88}
89
90/// A state file that could not be read.
91#[derive(Debug, thiserror::Error)]
92pub enum StateFileError {
93    #[error(transparent)]
94    Io(#[from] io::Error),
95    #[error("not a Celox state file")]
96    BadMagic,
97    #[error("unsupported state file version {0} (this build reads version {VERSION})")]
98    UnsupportedVersion(u16),
99    #[error("malformed state file: {0}")]
100    Malformed(String),
101}
102
103/// A state object whose saved values differ between two files.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum StateDifference {
106    /// Present in both files with a different width or value.
107    Changed {
108        path: String,
109        left: StateObject,
110        right: StateObject,
111    },
112    /// Present only in the left file.
113    OnlyLeft(StateObject),
114    /// Present only in the right file.
115    OnlyRight(StateObject),
116}
117
118impl StateFile {
119    pub fn write_to(&self, mut writer: impl Write) -> io::Result<()> {
120        let w = &mut writer;
121        w.write_all(MAGIC)?;
122        put_u16(w, VERSION)?;
123        let mut flags = 0;
124        if self.four_state {
125            flags |= FLAG_FOUR_STATE;
126        }
127        if self.schedule.is_some() {
128            flags |= FLAG_SCHEDULE;
129        }
130        put_u16(w, flags)?;
131        put_len(w, self.objects.len())?;
132        for object in &self.objects {
133            put_str(w, &object.path)?;
134            put_u64(w, object.width as u64)?;
135            w.write_all(&[
136                match object.role {
137                    StateRole::State => 0,
138                    StateRole::Comb => 1,
139                },
140                object.is_4state as u8,
141            ])?;
142            let size = object.width.div_ceil(8);
143            assert_eq!(object.value.len(), size, "value size of `{}`", object.path);
144            w.write_all(&object.value)?;
145            if self.four_state && object.is_4state {
146                let mask = object.mask.as_deref().unwrap_or_default();
147                assert_eq!(mask.len(), size, "mask size of `{}`", object.path);
148                w.write_all(mask)?;
149            }
150        }
151        if let Some(schedule) = &self.schedule {
152            put_u64(w, schedule.time)?;
153            put_len(w, schedule.clocks.len())?;
154            for (event, period) in &schedule.clocks {
155                put_str(w, event)?;
156                put_u64(w, *period)?;
157            }
158            put_len(w, schedule.events.len())?;
159            for event in &schedule.events {
160                put_event(w, event)?;
161            }
162            put_len(w, schedule.periodic.len())?;
163            for (event, count) in &schedule.periodic {
164                put_event(w, event)?;
165                put_u64(w, *count)?;
166            }
167            put_len(w, schedule.high_events.len())?;
168            for event in &schedule.high_events {
169                put_str(w, event)?;
170            }
171        }
172        Ok(())
173    }
174
175    pub fn read_from(mut reader: impl Read) -> Result<Self, StateFileError> {
176        let r = &mut reader;
177        let mut magic = [0; 8];
178        r.read_exact(&mut magic)?;
179        if &magic != MAGIC {
180            return Err(StateFileError::BadMagic);
181        }
182        let version = get_u16(r)?;
183        if version != VERSION {
184            return Err(StateFileError::UnsupportedVersion(version));
185        }
186        let flags = get_u16(r)?;
187        let four_state = flags & FLAG_FOUR_STATE != 0;
188        let count = get_u32(r)?;
189        let mut objects = Vec::new();
190        for _ in 0..count {
191            let path = get_str(r)?;
192            let width = usize::try_from(get_u64(r)?)
193                .map_err(|_| StateFileError::Malformed(format!("width of `{path}`")))?;
194            let mut tags = [0; 2];
195            r.read_exact(&mut tags)?;
196            let role = match tags[0] {
197                0 => StateRole::State,
198                1 => StateRole::Comb,
199                other => {
200                    return Err(StateFileError::Malformed(format!(
201                        "role {other} of `{path}`"
202                    )));
203                }
204            };
205            let is_4state = tags[1] != 0;
206            let size = width.div_ceil(8);
207            let value = get_bytes(r, size)?;
208            let mask = if four_state && is_4state {
209                Some(get_bytes(r, size)?)
210            } else {
211                None
212            };
213            objects.push(StateObject {
214                path,
215                width,
216                role,
217                is_4state,
218                value,
219                mask,
220            });
221        }
222        let schedule = if flags & FLAG_SCHEDULE != 0 {
223            let time = get_u64(r)?;
224            let clocks = (0..get_u32(r)?)
225                .map(|_| Ok((get_str(r)?, get_u64(r)?)))
226                .collect::<Result<_, StateFileError>>()?;
227            let events = (0..get_u32(r)?)
228                .map(|_| get_event(r))
229                .collect::<Result<_, _>>()?;
230            let periodic = (0..get_u32(r)?)
231                .map(|_| Ok((get_event(r)?, get_u64(r)?)))
232                .collect::<Result<_, StateFileError>>()?;
233            let high_events = (0..get_u32(r)?)
234                .map(|_| get_str(r))
235                .collect::<Result<_, _>>()?;
236            Some(ScheduleRecord {
237                time,
238                clocks,
239                events,
240                periodic,
241                high_events,
242            })
243        } else {
244            None
245        };
246        let mut rest = [0; 1];
247        if r.read(&mut rest)? != 0 {
248            return Err(StateFileError::Malformed(
249                "trailing bytes after the last section".into(),
250            ));
251        }
252        Ok(Self {
253            four_state,
254            objects,
255            schedule,
256        })
257    }
258
259    /// Objects whose saved values differ between `self` and `other`, by
260    /// path. Combinational objects are recomputed from the other state, so
261    /// they are compared only with `include_comb`; an object counts as
262    /// combinational if either file says so, which hides values that
263    /// optimizations left stale.
264    pub fn diff(&self, other: &Self, include_comb: bool) -> Vec<StateDifference> {
265        use std::collections::BTreeMap;
266
267        let left: BTreeMap<_, _> = self.objects.iter().map(|o| (&o.path, o)).collect();
268        let right: BTreeMap<_, _> = other.objects.iter().map(|o| (&o.path, o)).collect();
269        let compared = |object: &StateObject| include_comb || object.role == StateRole::State;
270        let mut differences = Vec::new();
271        for (path, l) in &left {
272            match right.get(path) {
273                Some(r) => {
274                    if !(compared(l) && compared(r)) {
275                        continue;
276                    }
277                    let lmask = l.mask.as_deref().unwrap_or_default();
278                    let rmask = r.mask.as_deref().unwrap_or_default();
279                    if l.width != r.width || l.value != r.value || !masks_equal(lmask, rmask) {
280                        differences.push(StateDifference::Changed {
281                            path: (*path).clone(),
282                            left: (*l).clone(),
283                            right: (*r).clone(),
284                        });
285                    }
286                }
287                None if compared(l) => differences.push(StateDifference::OnlyLeft((*l).clone())),
288                None => {}
289            }
290        }
291        for (path, r) in &right {
292            if !left.contains_key(path) && compared(r) {
293                differences.push(StateDifference::OnlyRight((*r).clone()));
294            }
295        }
296        differences
297    }
298}
299
300/// A missing mask means all bits are known.
301fn masks_equal(left: &[u8], right: &[u8]) -> bool {
302    let len = left.len().max(right.len());
303    (0..len).all(|i| left.get(i).copied().unwrap_or(0) == right.get(i).copied().unwrap_or(0))
304}
305
306/// Format little-endian bytes as a hexadecimal number of `width` bits, with
307/// X/Z bits from `mask` shown as `x`.
308pub fn format_state_value(value: &[u8], mask: Option<&[u8]>, width: usize) -> String {
309    let digits = width.div_ceil(4).max(1);
310    (0..digits)
311        .rev()
312        .map(|digit| {
313            let nibble = |bytes: &[u8]| {
314                let byte = bytes.get(digit / 2).copied().unwrap_or(0);
315                (byte >> ((digit % 2) * 4)) & 0xf
316            };
317            if mask.is_some_and(|mask| nibble(mask) != 0) {
318                'x'
319            } else {
320                char::from_digit(u32::from(nibble(value)), 16).unwrap()
321            }
322        })
323        .collect()
324}
325
326fn put_u16(w: &mut impl Write, value: u16) -> io::Result<()> {
327    w.write_all(&value.to_le_bytes())
328}
329
330fn put_u64(w: &mut impl Write, value: u64) -> io::Result<()> {
331    w.write_all(&value.to_le_bytes())
332}
333
334fn put_len(w: &mut impl Write, len: usize) -> io::Result<()> {
335    let len = u32::try_from(len)
336        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many entries"))?;
337    w.write_all(&len.to_le_bytes())
338}
339
340fn put_str(w: &mut impl Write, value: &str) -> io::Result<()> {
341    put_len(w, value.len())?;
342    w.write_all(value.as_bytes())
343}
344
345fn put_event(w: &mut impl Write, event: &ScheduledEvent) -> io::Result<()> {
346    put_u64(w, event.time)?;
347    put_str(w, &event.event)?;
348    put_str(w, &event.signal)?;
349    w.write_all(&[event.value])
350}
351
352fn get_u16(r: &mut impl Read) -> io::Result<u16> {
353    let mut bytes = [0; 2];
354    r.read_exact(&mut bytes)?;
355    Ok(u16::from_le_bytes(bytes))
356}
357
358fn get_u32(r: &mut impl Read) -> io::Result<u32> {
359    let mut bytes = [0; 4];
360    r.read_exact(&mut bytes)?;
361    Ok(u32::from_le_bytes(bytes))
362}
363
364fn get_u64(r: &mut impl Read) -> io::Result<u64> {
365    let mut bytes = [0; 8];
366    r.read_exact(&mut bytes)?;
367    Ok(u64::from_le_bytes(bytes))
368}
369
370fn get_bytes(r: &mut impl Read, len: usize) -> io::Result<Vec<u8>> {
371    // Read through `take` so a corrupt length fails at end of input instead
372    // of allocating the claimed size up front.
373    let mut bytes = Vec::new();
374    r.take(len as u64).read_to_end(&mut bytes)?;
375    if bytes.len() != len {
376        return Err(io::ErrorKind::UnexpectedEof.into());
377    }
378    Ok(bytes)
379}
380
381fn get_str(r: &mut impl Read) -> Result<String, StateFileError> {
382    let len = get_u32(r)? as usize;
383    String::from_utf8(get_bytes(r, len)?)
384        .map_err(|_| StateFileError::Malformed("path is not UTF-8".into()))
385}
386
387fn get_event(r: &mut impl Read) -> Result<ScheduledEvent, StateFileError> {
388    let time = get_u64(r)?;
389    let event = get_str(r)?;
390    let signal = get_str(r)?;
391    let mut value = [0; 1];
392    r.read_exact(&mut value)?;
393    Ok(ScheduledEvent {
394        time,
395        event,
396        signal,
397        value: value[0],
398    })
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    fn object(path: &str, role: StateRole, value: u8) -> StateObject {
406        StateObject {
407            path: path.into(),
408            width: 8,
409            role,
410            is_4state: true,
411            value: vec![value],
412            mask: Some(vec![0]),
413        }
414    }
415
416    fn sample() -> StateFile {
417        let event = ScheduledEvent {
418            time: 15,
419            event: "clk".into(),
420            signal: "clk".into(),
421            value: 1,
422        };
423        StateFile {
424            four_state: true,
425            objects: vec![
426                object("q", StateRole::State, 3),
427                StateObject {
428                    path: "wide".into(),
429                    width: 70,
430                    role: StateRole::State,
431                    is_4state: false,
432                    value: (0..9).collect(),
433                    mask: None,
434                },
435                object("y", StateRole::Comb, 4),
436            ],
437            schedule: Some(ScheduleRecord {
438                time: 10,
439                clocks: vec![("clk".into(), 10)],
440                events: vec![event.clone()],
441                periodic: vec![(event, 1)],
442                high_events: vec!["clk".into()],
443            }),
444        }
445    }
446
447    #[test]
448    fn round_trips() {
449        let file = sample();
450        let mut bytes = Vec::new();
451        file.write_to(&mut bytes).unwrap();
452        assert_eq!(StateFile::read_from(bytes.as_slice()).unwrap(), file);
453    }
454
455    #[test]
456    fn rejects_foreign_and_truncated_input() {
457        assert!(matches!(
458            StateFile::read_from(&b"not a state file"[..]),
459            Err(StateFileError::BadMagic)
460        ));
461        let mut bytes = Vec::new();
462        sample().write_to(&mut bytes).unwrap();
463        for len in [10, bytes.len() - 1] {
464            assert!(StateFile::read_from(&bytes[..len]).is_err());
465        }
466        bytes.push(0);
467        assert!(matches!(
468            StateFile::read_from(bytes.as_slice()),
469            Err(StateFileError::Malformed(_))
470        ));
471    }
472
473    #[test]
474    fn diff_ignores_combinational_objects_unless_asked() {
475        let left = sample();
476        let mut right = sample();
477        right.objects[0].value = vec![5];
478        right.objects[2].value = vec![9];
479        let paths = |differences: Vec<StateDifference>| -> Vec<String> {
480            differences
481                .into_iter()
482                .map(|difference| match difference {
483                    StateDifference::Changed { path, .. } => path,
484                    StateDifference::OnlyLeft(o) | StateDifference::OnlyRight(o) => o.path,
485                })
486                .collect()
487        };
488        assert_eq!(paths(left.diff(&right, false)), ["q"]);
489        assert_eq!(paths(left.diff(&right, true)), ["q", "y"]);
490
491        // A stale value under a state role on one side is still hidden when
492        // the other side knows the object is combinational.
493        right.objects[2].role = StateRole::State;
494        assert_eq!(paths(left.diff(&right, false)), ["q"]);
495    }
496
497    #[test]
498    fn formats_values_with_unknown_bits() {
499        assert_eq!(format_state_value(&[0x3c, 0x01], None, 12), "13c");
500        assert_eq!(format_state_value(&[0x3c], Some(&[0xf0]), 8), "xc");
501    }
502}