miden-debug-engine 0.6.1

Core debugger engine for miden-debug
Documentation
#[cfg(feature = "tui")]
use std::{ffi::OsStr, path::Path};

use miden_processor::{ExecutionOptions, StackInputs, advice::AdviceInputs};
use serde::Deserialize;

use crate::felt::Felt;

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(try_from = "ExecutionConfigFile")]
pub struct ExecutionConfig {
    pub inputs: StackInputs,
    pub advice_inputs: AdviceInputs,
    pub options: ExecutionOptions,
}

impl TryFrom<ExecutionConfigFile> for ExecutionConfig {
    type Error = String;

    #[inline]
    fn try_from(file: ExecutionConfigFile) -> Result<Self, Self::Error> {
        Self::from_inputs_file(file)
    }
}

impl ExecutionConfig {
    pub fn parse_file<P>(path: P) -> std::io::Result<Self>
    where
        P: AsRef<std::path::Path>,
    {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path)?;

        let file =
            toml::from_str::<ExecutionConfigFile>(&content).map_err(std::io::Error::other)?;
        Self::from_inputs_file(file).map_err(std::io::Error::other)
    }

    pub fn parse_str(content: &str) -> Result<Self, String> {
        let file = toml::from_str::<ExecutionConfigFile>(content).map_err(|err| err.to_string())?;

        Self::from_inputs_file(file)
    }

    fn from_inputs_file(file: ExecutionConfigFile) -> Result<Self, String> {
        let felts: Vec<_> = file.inputs.stack.into_iter().map(|felt| felt.0).collect();
        let inputs =
            StackInputs::new(&felts).map_err(|err| format!("invalid value for 'stack': {err}"))?;
        let advice_inputs = AdviceInputs::default()
            .with_stack(file.inputs.advice.stack.into_iter().map(|felt| felt.0))
            .with_map(file.inputs.advice.map.into_iter().map(|entry| {
                (entry.digest.0, entry.values.into_iter().map(|felt| felt.0).collect::<Vec<_>>())
            }));

        Ok(Self {
            inputs,
            advice_inputs,
            options: file.options,
        })
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
struct ExecutionConfigFile {
    inputs: Inputs,
    #[serde(deserialize_with = "deserialize_execution_options")]
    options: ExecutionOptions,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
struct Inputs {
    /// The contents of the operand stack, top is leftmost
    stack: Vec<Felt>,
    /// The inputs to the advice provider
    advice: Advice,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
struct Advice {
    /// The contents of the advice stack, top is leftmost
    stack: Vec<Felt>,
    /// Entries to populate the advice map with
    map: Vec<AdviceMapEntry>,
}

#[derive(Debug, Clone, Deserialize)]
struct AdviceMapEntry {
    digest: Word,
    /// Values that will be pushed to the advice stack when this entry is requested
    values: Vec<Felt>,
}

#[cfg(feature = "tui")]
impl clap::builder::ValueParserFactory for ExecutionConfig {
    type Parser = ExecutionConfigParser;

    fn value_parser() -> Self::Parser {
        ExecutionConfigParser
    }
}

#[cfg(feature = "tui")]
#[doc(hidden)]
#[derive(Clone)]
pub struct ExecutionConfigParser;

#[cfg(feature = "tui")]
impl clap::builder::TypedValueParser for ExecutionConfigParser {
    type Value = ExecutionConfig;

    fn parse_ref(
        &self,
        _cmd: &clap::Command,
        _arg: Option<&clap::Arg>,
        value: &OsStr,
    ) -> Result<Self::Value, clap::error::Error> {
        use clap::error::{Error, ErrorKind};

        let inputs_path = Path::new(value);
        if !inputs_path.is_file() {
            return Err(Error::raw(
                ErrorKind::InvalidValue,
                format!("invalid inputs file: '{}' is not a file", inputs_path.display()),
            ));
        }

        let content = std::fs::read_to_string(inputs_path).map_err(|err| {
            Error::raw(ErrorKind::ValueValidation, format!("failed to read inputs file: {err}"))
        })?;
        let inputs_file = toml::from_str::<ExecutionConfigFile>(&content).map_err(|err| {
            Error::raw(ErrorKind::ValueValidation, format!("invalid inputs file: {err}"))
        })?;

        ExecutionConfig::from_inputs_file(inputs_file).map_err(|err| {
            Error::raw(ErrorKind::ValueValidation, format!("invalid inputs file: {err}"))
        })
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Word(miden_core::Word);
impl<'de> Deserialize<'de> for Word {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let digest = String::deserialize(deserializer)?;
        miden_core::Word::try_from(&digest)
            .map_err(|err| serde::de::Error::custom(format!("invalid digest: {err}")))
            .map(Self)
    }
}

fn deserialize_execution_options<'de, D>(deserializer: D) -> Result<ExecutionOptions, D::Error>
where
    D: serde::Deserializer<'de>,
{
    #[derive(Default, Deserialize)]
    #[serde(default)]
    struct ExecOptions {
        max_cycles: Option<u32>,
        expected_cycles: u32,
    }

    ExecOptions::deserialize(deserializer).and_then(|opts| {
        ExecutionOptions::new(
            opts.max_cycles,
            opts.expected_cycles,
            ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
            /* enable_tracing= */ true,
            /* enable_debugging= */ true,
        )
        .map(|exec_opts| exec_opts.with_debugging(true))
        .map_err(|err| serde::de::Error::custom(format!("invalid execution options: {err}")))
    })
}

#[cfg(test)]
mod tests {
    use miden_processor::Felt as RawFelt;
    use toml::toml;

    use super::{ExecutionConfig, *};

    #[test]
    fn execution_config_empty() {
        let text = toml::to_string_pretty(&toml! {
            [inputs]
            [options]
        })
        .unwrap();

        let file = toml::from_str::<ExecutionConfig>(&text).unwrap();
        let expected_inputs = StackInputs::new(&[]).unwrap();
        assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
        assert!(file.advice_inputs.stack.is_empty());
        assert!(file.options.enable_tracing());
        assert!(file.options.enable_debugging());
        assert_eq!(file.options.max_cycles(), ExecutionOptions::MAX_CYCLES);
        assert_eq!(file.options.expected_cycles(), ExecutionOptions::default().expected_cycles());
    }

    #[test]
    fn execution_config_with_options() {
        let text = toml::to_string_pretty(&toml! {
            [inputs]
            [options]
            max_cycles = 100000
        })
        .unwrap();

        let file = ExecutionConfig::parse_str(&text).unwrap();
        let expected_inputs = StackInputs::new(&[]).unwrap();
        assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
        assert!(file.advice_inputs.stack.is_empty());
        assert!(file.options.enable_tracing());
        assert!(file.options.enable_debugging());
        assert_eq!(file.options.max_cycles(), 100000);
        assert_eq!(file.options.expected_cycles(), ExecutionOptions::default().expected_cycles());
    }

    #[test]
    fn execution_config_with_operands() {
        let text = toml::to_string_pretty(&toml! {
            [inputs]
            stack = [1, 2, 3]

            [options]
            max_cycles = 100000
        })
        .unwrap();

        let file = ExecutionConfig::parse_str(&text).unwrap();
        let expected_inputs =
            StackInputs::new(&[RawFelt::new(1), RawFelt::new(2), RawFelt::new(3)]).unwrap();
        assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
        assert!(file.advice_inputs.stack.is_empty());
        assert!(file.options.enable_tracing());
        assert!(file.options.enable_debugging());
        assert_eq!(file.options.max_cycles(), 100000);
        assert_eq!(file.options.expected_cycles(), ExecutionOptions::default().expected_cycles());
    }

    #[test]
    fn execution_config_with_advice() {
        let text = toml::to_string_pretty(&toml! {
            [inputs]
            stack = [1, 2, 0x3]

            [inputs.advice]
            stack = [1, 2, 3, 4]

            [[inputs.advice.map]]
            digest = "0x3cff5b58a573dc9d25fd3c57130cc57e5b1b381dc58b5ae3594b390c59835e63"
            values = [1, 2, 3, 4]

            [options]
            max_cycles = 100000
        })
        .unwrap();
        let digest = miden_core::Word::try_from(
            "0x3cff5b58a573dc9d25fd3c57130cc57e5b1b381dc58b5ae3594b390c59835e63",
        )
        .unwrap();
        let file = ExecutionConfig::parse_str(&text).unwrap_or_else(|err| panic!("{err}"));
        let expected_inputs =
            StackInputs::new(&[RawFelt::new(1), RawFelt::new(2), RawFelt::new(3)]).unwrap();
        assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
        assert_eq!(
            file.advice_inputs.stack,
            &[RawFelt::new(1), RawFelt::new(2), RawFelt::new(3), RawFelt::new(4)]
        );
        assert_eq!(
            file.advice_inputs.map.get(&digest).map(|value| value.as_ref()),
            Some([RawFelt::new(1), RawFelt::new(2), RawFelt::new(3), RawFelt::new(4)].as_slice())
        );
        assert!(file.options.enable_tracing());
        assert!(file.options.enable_debugging());
        assert_eq!(file.options.max_cycles(), 100000);
        assert_eq!(file.options.expected_cycles(), ExecutionOptions::default().expected_cycles());
    }
}