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