Skip to main content

miden_debug/
dap_server.rs

1use std::{path::Path, sync::Arc};
2
3use miden_assembly::{Assembler, DefaultSourceManager, SourceManager};
4use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
5use miden_core::{Word, events::EventId};
6use miden_debug_engine::HybridPackageRegistry;
7use miden_debug_types::{Location, SourceFile, SourceManagerExt, SourceSpan};
8use miden_mast_package::{Package, PackageId};
9use miden_package_registry::{PackageProvider, PackageRegistry};
10use miden_processor::{
11    BaseHost, DefaultHost, FutureMaybeSend, Host, HostLibrary, LoadedMastForest, ProcessorState,
12    advice::AdviceMutation, event::EventError,
13};
14
15use crate::{DapConfig, DapExecutor, DebuggerConfig, InputFile};
16
17/// Start a DAP server for a local Miden program.
18///
19/// This is the non-transaction counterpart to `miden-client exec --start-debug-adapter`.
20/// It accepts standalone MASM source files as well as compiled package artifacts.
21pub fn run(config: Box<DebuggerConfig>) -> Result<(), Report> {
22    let addr = config
23        .start_debug_adapter
24        .as_ref()
25        .ok_or_else(|| Report::msg("missing --start-debug-adapter address"))?;
26    DapConfig::set_global(
27        DapConfig::new(addr).with_source_path_prefixes(config.source_path_prefixes.clone()),
28    );
29
30    let source_manager = Arc::new(DefaultSourceManager::default());
31    let inputs = crate::program_loader::execution_inputs(&config)?;
32    let mut registry = HybridPackageRegistry::new(
33        config.sysroot.as_deref(),
34        &config.search_path,
35        &config.link_libraries,
36    )?;
37    let program = load_program(&config, source_manager.clone(), &mut registry)?;
38    let mut host = StandaloneDapHost::new(source_manager);
39
40    for package in registry.all() {
41        let name = package.name.clone();
42        host.load_library(package).map_err(|err| {
43            Report::msg(format!("failed to load package '{name}' into DAP host: {err}"))
44        })?;
45    }
46
47    let executor = DapExecutor::new(inputs.inputs, inputs.advice_inputs, inputs.options);
48    futures::executor::block_on(executor.execute_async(program, &mut host))
49        .map(|_| ())
50        .map_err(|err| Report::msg(format!("program execution failed: {err}")))
51}
52
53struct StandaloneDapHost {
54    inner: DefaultHost<DefaultSourceManager>,
55    source_manager: Arc<DefaultSourceManager>,
56}
57
58impl StandaloneDapHost {
59    fn new(source_manager: Arc<DefaultSourceManager>) -> Self {
60        let inner = DefaultHost::default().with_source_manager(source_manager.clone());
61        Self {
62            inner,
63            source_manager,
64        }
65    }
66
67    fn load_library(
68        &mut self,
69        lib: impl Into<HostLibrary>,
70    ) -> Result<(), miden_processor::ExecutionError> {
71        self.inner.load_library(lib)
72    }
73
74    fn ensure_source_file(&self, location: &Location) -> Option<Arc<SourceFile>> {
75        if let Some(file) = self.source_manager.get_by_uri(location.uri()) {
76            return Some(file);
77        }
78
79        let path = location
80            .uri()
81            .as_str()
82            .strip_prefix("file://")
83            .unwrap_or_else(|| location.uri().as_str());
84        self.source_manager.load_file(Path::new(path)).ok()
85    }
86}
87
88impl BaseHost for StandaloneDapHost {
89    fn get_label_and_source_file(
90        &self,
91        location: &Location,
92    ) -> (SourceSpan, Option<Arc<SourceFile>>) {
93        let maybe_file = self.ensure_source_file(location);
94        let span = self.source_manager.location_to_span(location.clone()).unwrap_or_default();
95        (span, maybe_file)
96    }
97
98    fn resolve_event(&self, event_id: EventId) -> Option<&miden_core::events::EventName> {
99        self.inner.resolve_event(event_id)
100    }
101}
102
103impl Host for StandaloneDapHost {
104    fn get_mast_forest(
105        &self,
106        node_digest: &Word,
107    ) -> impl FutureMaybeSend<Option<LoadedMastForest>> {
108        self.inner.get_mast_forest(node_digest)
109    }
110
111    fn on_event(
112        &mut self,
113        process: &ProcessorState<'_>,
114    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
115        self.inner.on_event(process)
116    }
117}
118
119fn load_program(
120    config: &DebuggerConfig,
121    source_manager: Arc<dyn SourceManager>,
122    registry: &mut HybridPackageRegistry,
123) -> Result<Arc<Package>, Report> {
124    let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
125    if let InputFile::Real(path) = input
126        && path.extension().and_then(|ext| ext.to_str()) == Some("masm")
127    {
128        return assemble_masm_program(path, source_manager, registry);
129    }
130
131    let package = load_package(config)?;
132    verify_package_dependencies(&package, registry)?;
133    assert!(package.is_program());
134    Ok(package)
135}
136
137fn assemble_masm_program(
138    path: &Path,
139    source_manager: Arc<dyn SourceManager>,
140    registry: &HybridPackageRegistry,
141) -> Result<Arc<Package>, Report> {
142    let mut assembler = Assembler::new(source_manager.clone());
143
144    let mut parser =
145        miden_assembly_syntax::ModuleParser::new(Some(miden_assembly::ast::ModuleKind::Executable));
146    let module = parser.parse_file(None, path, source_manager)?;
147
148    for extern_package in module.required_packages() {
149        let package_id = PackageId::from(extern_package.clone().into_inner());
150        let package = registry
151            .find_latest(&package_id, &miden_project::VersionReq::STAR.into())
152            .ok_or_else(|| Report::msg(format!("extern package '{package_id}' is not available")))
153            .and_then(|record| registry.load_package(&package_id, record.version()))?;
154        assembler.link_package(package, miden_project::Linkage::Dynamic)?;
155    }
156
157    assembler.assemble_program("program", module).map(Arc::from)
158}
159
160fn load_package(config: &DebuggerConfig) -> Result<Arc<Package>, Report> {
161    let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
162    let package = match input {
163        InputFile::Real(path) => {
164            let bytes = std::fs::read(path).into_diagnostic()?;
165            Package::read_from_bytes_unchecked(&bytes).map(Arc::new).map_err(|err| {
166                Report::msg(format!("failed to load Miden package from {}: {err}", path.display()))
167            })?
168        }
169        InputFile::Stdin(bytes) => {
170            Package::read_from_bytes_unchecked(bytes).map(Arc::new).map_err(|err| {
171                Report::msg(format!("failed to load Miden package from stdin: {err}"))
172            })?
173        }
174    };
175
176    if let Some(entry) = config.entrypoint.as_ref() {
177        let id = entry
178            .parse::<miden_assembly::ast::QualifiedProcedureName>()
179            .map_err(|_| Report::msg(format!("invalid function identifier: '{entry}'")))?;
180        if !package.is_library() {
181            return Err(Report::msg("cannot use --entrypoint with executable packages"));
182        }
183
184        package.make_executable(&id).map(Arc::new)
185    } else {
186        Ok(package)
187    }
188}
189
190fn verify_package_dependencies(
191    package: &Package,
192    registry: &HybridPackageRegistry,
193) -> Result<(), Report> {
194    for dependency in package.manifest.dependencies() {
195        let version = miden_project::Version::new(dependency.version().clone(), dependency.digest);
196        if !registry.is_version_available(&dependency.name, &version) {
197            return Err(Report::msg(format!(
198                "dependency {}@{version} not found in loaded libraries",
199                dependency.name
200            )));
201        }
202    }
203
204    Ok(())
205}