From f3e21534d4bcb5209af4a649386822919849a9d5 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 3 Aug 2026 12:22:24 +0200 Subject: [PATCH 01/42] don't force intronsic results into memory --- src/tools/miri/src/intrinsics/math.rs | 8 ++++---- src/tools/miri/src/intrinsics/mod.rs | 14 +++++--------- src/tools/miri/src/intrinsics/simd.rs | 2 +- src/tools/miri/src/math.rs | 4 ++-- src/tools/miri/src/shims/foreign_items.rs | 2 +- src/tools/miri/src/shims/mod.rs | 4 ++-- src/tools/miri/src/shims/unwind.rs | 7 ++++--- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/tools/miri/src/intrinsics/math.rs b/src/tools/miri/src/intrinsics/math.rs index adb768e6bcffc..ad3881b0e6a6d 100644 --- a/src/tools/miri/src/intrinsics/math.rs +++ b/src/tools/miri/src/intrinsics/math.rs @@ -10,7 +10,7 @@ use crate::*; fn sqrt<'tcx, F: Float + FloatConvert + Into>( this: &mut MiriInterpCx<'tcx>, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx> { let [f] = check_intrinsic_arg_count(args)?; math::sqrt_op::(this, f, dest) @@ -45,7 +45,7 @@ fn is_host_unary_float_op(intrinsic_name: &str) -> Option<(FloatTy, HostUnaryFlo fn pow_intrinsic<'tcx, S: Semantics>( this: &mut MiriInterpCx<'tcx>, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx, ()> where IeeeFloat: HostFloatOperation + IeeeExt + Float + Into, @@ -69,7 +69,7 @@ where fn powi_intrinsic<'tcx, S: Semantics>( this: &mut MiriInterpCx<'tcx>, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx, ()> where IeeeFloat: HostFloatOperation + IeeeExt + Float + Into, @@ -98,7 +98,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { intrinsic_name: &str, _generic_args: ty::GenericArgsRef<'tcx>, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx, EmulateItemResult> { let this = self.eval_context_mut(); diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 0f55009db790b..7d7081fb609fb 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -53,12 +53,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let intrinsic_name = this.tcx.item_name(instance.def_id()); let intrinsic_name = intrinsic_name.as_str(); - // FIXME: avoid allocating memory - let dest = this.force_allocation(dest)?; - - let res = - this.emulate_intrinsic_by_name(intrinsic_name, instance.args, args, &dest, ret)?; - res.jump_to_next_block(this, &dest, ret, Some(unwind), |this| { + let res = this.emulate_intrinsic_by_name(intrinsic_name, instance.args, args, dest, ret)?; + res.jump_to_next_block(this, dest, ret, Some(unwind), |this| { // We haven't handled the intrinsic, let's see if we can use a fallback body. if this.tcx.intrinsic(instance.def_id()).unwrap().must_be_overridden { throw_unsup_format!("unimplemented intrinsic: `{intrinsic_name}`") @@ -88,7 +84,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { intrinsic_name: &str, generic_args: ty::GenericArgsRef<'tcx>, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ret: Option, ) -> InterpResult<'tcx, EmulateItemResult> { let this = self.eval_context_mut(); @@ -165,7 +161,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let link_name = this.tcx.codegen_fn_attrs(instance.def_id()).symbol_name.unwrap(); - // FIXME: avoid allocating memory + // These are anyway mostly vector intrinsics and vectors live in memory. let dest = this.force_allocation(dest)?; let res = 'handled: { @@ -250,7 +246,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; // The rest either implements the logic, or falls back to `lookup_exported_symbol`. - res.jump_to_next_block(this, &dest, ret, None, |this| { + res.jump_to_next_block(this, &dest.clone().into(), ret, None, |this| { throw_machine_stop!(TerminationInfo::UnsupportedForeignItem(format!( "can't call LLVM intrinsic `{link_name}` on architecture `{arch}`", arch = this.tcx.sess.target.arch, diff --git a/src/tools/miri/src/intrinsics/simd.rs b/src/tools/miri/src/intrinsics/simd.rs index 74582bc58900e..1f2fd9a8a64df 100644 --- a/src/tools/miri/src/intrinsics/simd.rs +++ b/src/tools/miri/src/intrinsics/simd.rs @@ -14,7 +14,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { &mut self, intrinsic_name: &str, args: &[OpTy<'tcx>], - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx, EmulateItemResult> { let this = self.eval_context_mut(); match intrinsic_name { diff --git a/src/tools/miri/src/math.rs b/src/tools/miri/src/math.rs index f67831839b711..1cacc9dde86f8 100644 --- a/src/tools/miri/src/math.rs +++ b/src/tools/miri/src/math.rs @@ -462,7 +462,7 @@ pub(crate) fn sqrt(x: F) -> F { pub fn sqrt_op<'tcx, F: Float + FloatConvert + Into>( this: &mut MiriInterpCx<'tcx>, f: &OpTy<'tcx>, - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx> { let f: F = this.read_scalar(f)?.to_float()?; // Sqrt is specified to be fully precise. @@ -536,7 +536,7 @@ pub fn host_unary_float_op<'tcx, S: Semantics>( this: &mut MiriInterpCx<'tcx>, f: &OpTy<'tcx>, op: HostUnaryFloatOp, - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ) -> InterpResult<'tcx> where IeeeFloat: HostFloatOperation + IeeeExt + Float + Into, diff --git a/src/tools/miri/src/shims/foreign_items.rs b/src/tools/miri/src/shims/foreign_items.rs index 683e9095f9b0c..a904116017876 100644 --- a/src/tools/miri/src/shims/foreign_items.rs +++ b/src/tools/miri/src/shims/foreign_items.rs @@ -74,7 +74,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // The rest either implements the logic, or falls back to `lookup_exported_symbol`. let res = this.emulate_foreign_item_inner(link_name, abi, args, &dest)?; - res.jump_to_next_block(this, &dest, ret, Some(unwind), |this| { + res.jump_to_next_block(this, &dest.clone().into(), ret, Some(unwind), |this| { if let Some(body) = this.lookup_exported_symbol(link_name)? { return interp_ok(Some(body)); } diff --git a/src/tools/miri/src/shims/mod.rs b/src/tools/miri/src/shims/mod.rs index 56466b5f3a1f7..a41f1a5c8ec42 100644 --- a/src/tools/miri/src/shims/mod.rs +++ b/src/tools/miri/src/shims/mod.rs @@ -43,7 +43,7 @@ impl EmulateItemResult { pub fn jump_to_next_block<'tcx, T: Default>( self, ecx: &mut crate::MiriInterpCx<'tcx>, - dest: &crate::MPlaceTy<'tcx>, + dest: &crate::PlaceTy<'tcx>, ret: Option, unwind: Option, not_supported: impl FnOnce(&mut crate::MiriInterpCx<'tcx>) -> crate::InterpResult<'tcx, T>, @@ -52,7 +52,7 @@ impl EmulateItemResult { match self { EmulateItemResult::NeedsReturn => { - trace!("{:?}", ecx.dump_place(&dest.clone().into())); + trace!("{:?}", ecx.dump_place(dest)); ecx.return_to_block(ret)?; interp_ok(T::default()) } diff --git a/src/tools/miri/src/shims/unwind.rs b/src/tools/miri/src/shims/unwind.rs index e8a804a8b023b..820a78725eedc 100644 --- a/src/tools/miri/src/shims/unwind.rs +++ b/src/tools/miri/src/shims/unwind.rs @@ -62,7 +62,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { try_fn: &OpTy<'tcx>, data: &OpTy<'tcx>, catch_fn: &OpTy<'tcx>, - dest: &MPlaceTy<'tcx>, + dest: &PlaceTy<'tcx>, ret: Option, ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); @@ -82,6 +82,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let try_fn = this.read_pointer(try_fn)?; let data = this.read_immediate(data)?; let catch_fn = this.read_pointer(catch_fn)?; + let dest = this.force_allocation(dest)?; // needs to be valid across fn calls // Now we make a function call, and pass `data` as first and only argument. let f_instance = this.get_ptr_fn(try_fn)?.as_instance()?; @@ -97,14 +98,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { )?; // We ourselves will return `0`, eventually (will be overwritten if we catch a panic). - this.write_null(dest)?; + this.write_null(&dest)?; // In unwind mode, we tag this frame with the extra data needed to catch unwinding. // This lets `handle_stack_pop` (below) know that we should stop unwinding // when we pop this frame. if this.tcx.sess.panic_strategy() == PanicStrategy::Unwind { this.frame_mut().extra.catch_unwind = - Some(CatchUnwindData { catch_fn, data, dest: dest.clone(), ret }); + Some(CatchUnwindData { catch_fn, data, dest, ret }); } interp_ok(()) From df4b2ec75ec3625353d87178f4aff717fcc7a470 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 5 Aug 2026 08:35:42 +0200 Subject: [PATCH 02/42] Prepare for merging from rust-lang/rust This updates the rust-version file to 7218ebe93668f51a94a572b690c433dfdbdc2c3d. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index 2f35beeceda8c..f29c624515673 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -73dc9167f1cd099e525c9ade2e068d1907b78564 +7218ebe93668f51a94a572b690c433dfdbdc2c3d From e6a0fa2592f1be3df9091450e0069385afdb0333 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 5 Aug 2026 08:36:47 +0200 Subject: [PATCH 03/42] fmt --- src/tools/miri/src/diagnostics.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tools/miri/src/diagnostics.rs b/src/tools/miri/src/diagnostics.rs index d4fe89d4f0258..7e8c49bf9fba0 100644 --- a/src/tools/miri/src/diagnostics.rs +++ b/src/tools/miri/src/diagnostics.rs @@ -372,10 +372,7 @@ pub fn report_result<'tcx>( .. }) => { ecx.handle_ice(); // print interpreter backtrace (this is outside the eval `catch_unwind`) - bug!( - "This validation error should be impossible in Miri: {}", - res.to_string() - ); + bug!("This validation error should be impossible in Miri: {}", res.to_string()); } UndefinedBehavior(_) => "Undefined Behavior", ResourceExhaustion(_) => "resource exhaustion", From efc7c7497b30548d628f3994a48916fae5b6eb88 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 5 Aug 2026 08:39:45 +0200 Subject: [PATCH 04/42] remove readdir_r now that we no longer need it --- src/tools/miri/src/shims/unix/fs.rs | 82 ------------------- .../src/shims/unix/macos/foreign_items.rs | 6 -- src/tools/miri/tests/pass-dep/libc/libc-fs.rs | 48 ----------- 3 files changed, 136 deletions(-) diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index c72d85bb87341..8594e7ea35e4e 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -1255,88 +1255,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(()) } - fn macos_readdir_r( - &mut self, - dirp_op: &OpTy<'tcx>, - entry_op: &OpTy<'tcx>, - result_op: &OpTy<'tcx>, - ) -> InterpResult<'tcx, Scalar> { - let this = self.eval_context_mut(); - - this.assert_target_os(Os::MacOs, "readdir_r"); - - let dirp = this.read_target_usize(dirp_op)?; - let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?; - - // Reject if isolation is enabled. - if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op { - this.reject_in_isolation("`readdir_r`", reject_with)?; - // Return error code, do *not* set `errno`. - return interp_ok(this.eval_libc("EBADF")); - } - - let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| { - err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir") - })?; - interp_ok(match open_dir.next_host_entry() { - Some(Ok(dir_entry)) => { - let dir_entry = this.dir_entry_fields(dir_entry)?; - // Write into entry, write pointer to result, return 0 on success. - // The name is written with write_os_str_to_c_str, while the rest of the - // dirent struct is written using write_int_fields. - - // For reference, on macOS this looks like: - // pub struct dirent { - // pub d_ino: u64, - // pub d_seekoff: u64, - // pub d_reclen: u16, - // pub d_namlen: u16, - // pub d_type: u8, - // pub d_name: [c_char; 1024], - // } - - let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?; - - // Write the name. - let name_place = this.project_field_named(&entry_place, "d_name")?; - let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str( - &dir_entry.name, - name_place.ptr(), - name_place.layout.size.bytes(), - )?; - if !name_fits { - throw_unsup_format!( - "a directory entry had a name too large to fit in libc::dirent" - ); - } - - // Write the other fields. - this.write_int_fields_named( - &[ - ("d_reclen", entry_place.layout.size.bytes().into()), - ("d_namlen", file_name_buf_len.strict_sub(1).into()), - ("d_type", dir_entry.d_type.into()), - ("d_ino", dir_entry.ino.into()), - ("d_seekoff", 0), - ], - &entry_place, - )?; - this.write_scalar(this.read_scalar(entry_op)?, &result_place)?; - - Scalar::from_i32(0) - } - None => { - // end of stream: return 0, assign *result=NULL - this.write_null(&result_place)?; - Scalar::from_i32(0) - } - Some(Err(e)) => { - // return positive error number on error (do *not* set last error) - this.host_error_to_errnum(e)? - } - }) - } - fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { let this = self.eval_context_mut(); diff --git a/src/tools/miri/src/shims/unix/macos/foreign_items.rs b/src/tools/miri/src/shims/unix/macos/foreign_items.rs index 3289d569173f4..9254031a8a4d1 100644 --- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs @@ -71,12 +71,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let [dirp] = this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?; this.readdir(dirp, dest)?; } - "readdir_r" | "readdir_r$INODE64" => { - let [dirp, entry, result] = - this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?; - let result = this.macos_readdir_r(dirp, entry, result)?; - this.write_scalar(result, dest)?; - } "realpath$DARWIN_EXTSN" => { let [path, resolved_path] = this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?; diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs index 29f32df2dd0eb..647012eb8f7cf 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs @@ -60,8 +60,6 @@ fn main() { test_ioctl(); test_opendir_closedir(); test_readdir(); - #[cfg(target_os = "macos")] - test_readdir_r(); #[cfg(target_os = "linux")] test_statx_on_file_path(); #[cfg(target_os = "linux")] @@ -1023,52 +1021,6 @@ fn test_readdir() { remove_dir(&dir_path).unwrap(); } -// We only support `readdir_r` on macOS. -// (It is deprecated so we don't want to add more support.) -#[cfg(target_os = "macos")] -fn test_readdir_r() { - use std::fs::{create_dir, remove_dir, write}; - use std::mem::MaybeUninit; - - let dir_path = utils::prepare_dir("miri_test_libc_readdir_r"); - create_dir(&dir_path).ok(); - - // Create test files - let file1 = dir_path.join("file1.txt"); - let file2 = dir_path.join("file2.txt"); - write(&file1, b"content1").unwrap(); - write(&file2, b"content2").unwrap(); - - let c_path = CString::new(dir_path.as_os_str().as_bytes()).unwrap(); - - unsafe { - let dirp = libc::opendir(c_path.as_ptr()); - assert!(!dirp.is_null()); - let mut entries = Vec::new(); - loop { - let mut entry: MaybeUninit = MaybeUninit::uninit(); - let mut result: *mut libc::dirent = std::ptr::null_mut(); - let ret = libc::readdir_r(dirp, entry.as_mut_ptr(), &mut result); - assert_eq!(ret, 0); - let entry_ptr = result; - if entry_ptr.is_null() { - break; - } - let name_ptr = std::ptr::addr_of!((*entry_ptr).d_name) as *const libc::c_char; - let name = CStr::from_ptr(name_ptr); - let name_str = name.to_string_lossy(); - entries.push(name_str.into_owned()); - } - assert_eq!(libc::closedir(dirp), 0); - entries.sort(); - assert_eq!(&entries, &[".", "..", "file1.txt", "file2.txt"]); - } - - remove_file(&file1).unwrap(); - remove_file(&file2).unwrap(); - remove_dir(&dir_path).unwrap(); -} - /// Check that all common fields of a `stat` struct are initialized. pub fn check_stat_fields(stat: &libc::stat) { let _st_nlink = stat.st_nlink; From ec46f102ea5df40b7c8a20312613a694041e3321 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 5 Aug 2026 08:41:53 +0200 Subject: [PATCH 05/42] fix priroda build --- src/tools/miri/priroda/src/main.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/priroda/src/main.rs b/src/tools/miri/priroda/src/main.rs index bc0dacc589a79..4978b205b8a99 100644 --- a/src/tools/miri/priroda/src/main.rs +++ b/src/tools/miri/priroda/src/main.rs @@ -22,7 +22,7 @@ use std::ops::Range; use std::path::PathBuf; use miri::Immediate::Uninit; -use miri::{interpret, *}; +use miri::*; use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; use rustc_driver::Compilation; use rustc_hir::attrs::CrateType; @@ -688,7 +688,7 @@ impl<'tcx> PrirodaContext<'tcx> { Either::Left(mplace) => match self.render_mplace_bytes(&mplace).report_err() { Ok(bytes) => bytes, - Err(err) => format!("", interpret::format_interp_error(err)), + Err(err) => format!("", err.to_string()), }, } } @@ -857,9 +857,7 @@ impl<'tcx> PrirodaContext<'tcx> { .ecx .eval_place_to_op(*place, None) .map(|op| self.render_source_shaped_op(op)) - .unwrap_or_else(|err| { - format!("", interpret::format_interp_error(err)) - }); + .unwrap_or_else(|err| format!("", err.to_string())); local_descs.push(LocalDesc { source_name: Some(var_debug_info.name), From 50ad30b6de63b57773d6de5a5588b80a2c9fb9f6 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 29 Jul 2026 22:49:32 +0300 Subject: [PATCH 06/42] [Priroda] Extract CLI command result rendering Move command-result printing into a helper and keep CLI loop control at the call site. --- src/tools/miri/priroda/src/main.rs | 154 +++++++++++++++-------------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/src/tools/miri/priroda/src/main.rs b/src/tools/miri/priroda/src/main.rs index 4978b205b8a99..acff1b8666419 100644 --- a/src/tools/miri/priroda/src/main.rs +++ b/src/tools/miri/priroda/src/main.rs @@ -920,78 +920,10 @@ impl Cli { } if let Some(command) = self.parse_command(&input) { - match session.run_command(command)? { - CommandResult::ExecutionStopped(result) => { - if matches!(result, StepResult::Breakpoint) { - println!("Hit breakpoint"); - } - self.print_location(session); - } - CommandResult::BreakpointResult(res) => - match res { - BreakpointSetResult::Added(path, line) => - println!("breakpoint added: {}:{}", path.display(), line), - - BreakpointSetResult::Duplicate => println!("Duplicate breakpoint"), - }, - CommandResult::Locals(locals_desc) => - if locals_desc.is_empty() { - println!("no locals"); - } else { - for local_desc in &locals_desc { - let source_projection = local_desc - .source_projection - .as_ref() - .map(|fields| { - fields - .iter() - .map(|field| field.to_string()) - .collect::() - }) - .unwrap_or_default(); - - let name = local_desc - .source_name - .map_or_else(|| "".to_string(), |name| name.to_string()); - - let display_name = format!("{name}{source_projection}"); - - let local_id = local_desc.local.map_or_else( - || "".to_string(), - |local_idx| format!("_{}", local_idx.index()), - ); - - let storage_projection = local_desc - .storage_projection - .iter() - .map(StorageProj::render) - .collect::(); - - let display_local_id = format!("{local_id}{storage_projection}"); - println!( - "Name: {}, Id: {}, Ty: {}, Value: {}", - display_name, display_local_id, local_desc.ty, local_desc.value - ); - } - }, - CommandResult::SingleLocal(local_desc) => - match local_desc { - Some(local_desc) => { - println!( - "Id: _{}, Ty: {}, Value: {}", - local_desc.local.unwrap().index(), - local_desc.ty, - local_desc.value - ); - } - None => println!("no local for this id"), - }, - CommandResult::Memory(memory) => println!("{memory}"), - CommandResult::TerminateSession => { - println!("quitting"); - return interp_ok(()); - } - } + let command_res = session.run_command(command)?; + if !Self::print_command_result(command_res, session)? { + return interp_ok(()); + }; } else { println!("no command"); } @@ -1000,6 +932,82 @@ impl Cli { } } + fn print_command_result<'tcx>( + command_res: CommandResult, + session: &PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, bool> { + match command_res { + CommandResult::ExecutionStopped(result) => { + if matches!(result, StepResult::Breakpoint) { + println!("Hit breakpoint"); + } + Self::print_location(session); + } + CommandResult::BreakpointResult(res) => + match res { + BreakpointSetResult::Added(path, line) => + println!("breakpoint added: {}:{}", path.display(), line), + + BreakpointSetResult::Duplicate => println!("Duplicate breakpoint"), + }, + CommandResult::Locals(locals_desc) => + if locals_desc.is_empty() { + println!("no locals"); + } else { + for local_desc in &locals_desc { + let source_projection = local_desc + .source_projection + .as_ref() + .map(|fields| { + fields.iter().map(|field| field.to_string()).collect::() + }) + .unwrap_or_default(); + + let name = local_desc + .source_name + .map_or_else(|| "".to_string(), |name| name.to_string()); + + let display_name = format!("{name}{source_projection}"); + + let local_id = local_desc.local.map_or_else( + || "".to_string(), + |local_idx| format!("_{}", local_idx.index()), + ); + + let storage_projection = local_desc + .storage_projection + .iter() + .map(StorageProj::render) + .collect::(); + + let display_local_id = format!("{local_id}{storage_projection}"); + println!( + "Name: {}, Id: {}, Ty: {}, Value: {}", + display_name, display_local_id, local_desc.ty, local_desc.value + ); + } + }, + CommandResult::SingleLocal(local_desc) => + match local_desc { + Some(local_desc) => { + println!( + "Id: _{}, Ty: {}, Value: {}", + local_desc.local.unwrap().index(), + local_desc.ty, + local_desc.value + ); + } + None => println!("no local for this id"), + }, + CommandResult::Memory(memory) => println!("{memory}"), + CommandResult::TerminateSession => { + println!("quitting"); + return interp_ok(false); + } + } + interp_ok(true) + } + fn parse_command(&self, input: &str) -> Option { // TODO: look at the Spanned crate for how to easily produce errors in // rustc's style while manually parsing text input. @@ -1024,7 +1032,7 @@ impl Cli { } } - fn print_location(&self, session: &PrirodaContext) { + fn print_location(session: &PrirodaContext) { match &session.current_location { Some(location) => if let Some(path) = session.local_path(location) { From 049c9da058b891a3a054685e47e6be7caf03bc1d Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 29 Jul 2026 23:22:40 +0300 Subject: [PATCH 07/42] [Priroda] Add initial DAP frontend selection Consume Priroda's --dap flag before handing arguments to rustc_driver::run_compiler, then dispatch the freshly-created PrirodaContext to either the existing CLI loop or a new DAP loop stub. --- src/tools/miri/priroda/src/main.rs | 65 ++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/tools/miri/priroda/src/main.rs b/src/tools/miri/priroda/src/main.rs index acff1b8666419..bb2327c8338e0 100644 --- a/src/tools/miri/priroda/src/main.rs +++ b/src/tools/miri/priroda/src/main.rs @@ -46,6 +46,7 @@ fn main() { rustc_driver::init_rustc_env_logger(&early_dcx); let mut args: Vec = std::env::args().collect(); + let frontend = Frontend::parse_from_args(&mut args); args.splice(1..1, miri::MIRI_DEFAULT_ARGS.iter().map(ToString::to_string)); @@ -55,15 +56,48 @@ fn main() { args.push(find_sysroot()); } // FIXME: handle the same `-Z` flags that Miri accepts. - rustc_driver::run_compiler(&args, &mut PrirodaCompilerCalls::new()); + rustc_driver::run_compiler(&args, &mut PrirodaCompilerCalls::new(frontend)); } -struct PrirodaCompilerCalls; +/// Frontend selected by Priroda-specific CLI flags. +#[derive(Clone, Copy)] +enum Frontend { + Cli, + Dap, +} + +impl Frontend { + /// Remove Priroda-only flags before forwarding the remaining arguments to rustc. + fn parse_from_args(args: &mut Vec) -> Self { + let mut frontend = Frontend::Cli; + let mut rustc_args = Vec::with_capacity(args.len()); + let mut parsing_priroda_args = true; + + for (idx, arg) in args.drain(..).enumerate() { + if idx != 0 && parsing_priroda_args && arg == "--dap" { + frontend = Frontend::Dap; + continue; + } + + if arg == "--" { + parsing_priroda_args = false; + } + + rustc_args.push(arg); + } + + *args = rustc_args; + frontend + } +} + +struct PrirodaCompilerCalls { + frontend: Frontend, +} impl PrirodaCompilerCalls { - // FIXME: remove this constructor if PrirodaCompilerCalls remains a unit struct. - fn new() -> Self { - Self + fn new(frontend: Frontend) -> Self { + Self { frontend } } } @@ -80,8 +114,16 @@ impl rustc_driver::Callbacks for PrirodaCompilerCalls { let ecx = create_ecx(tcx); let mut session = PrirodaContext::new(ecx); - let cli = Cli {}; - let result = cli.run_cli_loop(&mut session); + let result = match self.frontend { + Frontend::Cli => { + let cli = Cli {}; + cli.run_cli_loop(&mut session) + } + Frontend::Dap => { + let dap = Dap {}; + dap.run_dap_loop(&mut session) + } + }; match result.report_err() { Ok(()) => {} @@ -1075,3 +1117,12 @@ impl Cli { Some(DebuggerCommand::Follow(alloc_id, offset)) } } + +struct Dap; + +impl Dap { + pub fn run_dap_loop<'tcx>(&self, _session: &mut PrirodaContext<'tcx>) -> InterpResult<'tcx> { + // FIXME: implement DAP framing and request dispatch on top of PrirodaContext. + interp_ok(()) + } +} From 7b174e3b1adb76adada454c28033bd9988e538f6 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 29 Jul 2026 23:22:40 +0300 Subject: [PATCH 08/42] [Priroda] Add DAP UI fixtures --- src/tools/miri/priroda/tests/ui/dap_initialize.rs | 3 +++ src/tools/miri/priroda/tests/ui/dap_initialize.stdin | 3 +++ src/tools/miri/priroda/tests/ui/dap_initialize.stdout | 0 .../miri/priroda/tests/ui/dap_rejects_non_initialize_first.rs | 3 +++ .../priroda/tests/ui/dap_rejects_non_initialize_first.stdin | 3 +++ .../priroda/tests/ui/dap_rejects_non_initialize_first.stdout | 0 6 files changed, 12 insertions(+) create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize.stdout create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.rs b/src/tools/miri/priroda/tests/ui/dap_initialize.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdin b/src/tools/miri/priroda/tests/ui/dap_initialize.stdin new file mode 100644 index 0000000000000..873743fad394b --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdin @@ -0,0 +1,3 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.rs b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdin b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdin new file mode 100644 index 0000000000000..6b8fb8e08484a --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdin @@ -0,0 +1,3 @@ +Content-Length: 70 + +{"seq":2,"type":"request","command":"next","arguments":{"threadId":1}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout new file mode 100644 index 0000000000000..e69de29bb2d1d From 1544ae76ddb9cecfebf0bab3ddba33e85bd05a4b Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 29 Jul 2026 23:38:08 +0300 Subject: [PATCH 09/42] [Priroda] Split debugger frontends into modules --- src/tools/miri/priroda/src/debugger.rs | 823 ++++++++++++++++ src/tools/miri/priroda/src/frontend/cli.rs | 176 ++++ src/tools/miri/priroda/src/frontend/dap.rs | 17 + src/tools/miri/priroda/src/frontend/mod.rs | 5 + src/tools/miri/priroda/src/main.rs | 1001 +------------------- 5 files changed, 1027 insertions(+), 995 deletions(-) create mode 100644 src/tools/miri/priroda/src/debugger.rs create mode 100644 src/tools/miri/priroda/src/frontend/cli.rs create mode 100644 src/tools/miri/priroda/src/frontend/dap.rs create mode 100644 src/tools/miri/priroda/src/frontend/mod.rs diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs new file mode 100644 index 0000000000000..aa4cb45b85aba --- /dev/null +++ b/src/tools/miri/priroda/src/debugger.rs @@ -0,0 +1,823 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::Range; +use std::path::PathBuf; + +use miri::Immediate::Uninit; +use miri::{interpret, *}; +use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; +use rustc_hir::def::CtorKind; +use rustc_middle::mir::interpret::AllocId; +use rustc_middle::mir::{self, Local, ProjectionElem, VarDebugInfoContents, VarDebugInfoFragment}; +use rustc_middle::ty::{self, TyKind}; +use rustc_span::source_map::SourceMap; +use rustc_span::{Span, Symbol}; + +/// Structured source information for frontends. +pub(super) struct SourceLocation { + // storing `span` to use it lazily to compute path. + pub(super) span: Span, + pub(super) line: usize, +} + +impl SourceLocation { + fn local_path(&self, source_map: &SourceMap) -> Option { + let loc = source_map.lookup_char_pos(self.span.lo()); + loc.file.name.clone().into_local_path().map(normalize_path) + } +} + +/// Source-level breakpoints indexed by normalized path, then line. +type BreakpointTable = HashMap>; + +/// Owns one interpreter session and its debugger state. +/// +/// Frontend rendering should eventually live outside this type. +pub(super) struct PrirodaContext<'tcx> { + pub(super) ecx: MiriInterpCx<'tcx>, + breakpoints: BreakpointTable, + pub(super) current_location: Option, + last_location: Option, +} + +pub(super) enum StorageProj { + Field(usize), + Deref, + Downcast(Symbol), + Variant(usize), + Unsupported(String), +} + +impl StorageProj { + pub(super) fn render(&self) -> String { + match self { + StorageProj::Field(field_idx) => format!(".{field_idx}"), + StorageProj::Deref => ".*".to_string(), + StorageProj::Downcast(name) => format!(" as {name}"), + StorageProj::Variant(variant_idx) => format!(" as variant#{variant_idx}"), + StorageProj::Unsupported(unsop) => format!("."), + } + } +} + +pub(super) struct LocalDesc { + /// Source variable name from `VarDebugInfo`, if this row has one. + pub(super) source_name: Option, + + /// Source-side projection from `VarDebugInfo::composite`, e.g. `.field` in source fragment `x.field`. + pub(super) source_projection: Option>, + + /// MIR storage local that backs this description, if any. + pub(super) local: Option, + + /// rendered/debug MIR place projection for now + pub(super) storage_projection: Vec, + + /// Display-rendered type for this description. + pub(super) ty: String, + + /// Run-time state for now; will be expanded later + pub(super) value: String, +} + +impl LocalDesc { + pub(super) fn source_projection_str(&self) -> String { + self.source_projection + .as_ref() + .map(|fields| fields.iter().map(|field| field.to_string()).collect::()) + .unwrap_or_default() + } + + pub(super) fn storage_projection_str(&self) -> String { + self.storage_projection.iter().map(StorageProj::render).collect::() + } +} + +/// Controls when execution returns to the frontend. +enum ResumeMode { + /// Stop at the next visible MIR instruction. + MirInstruction, + /// Stop at the next source line + /// + /// Take `Option` because some cases current state has no mapped to source code location + SourceLine(Option<(PathBuf, usize)>), + /// Continue until reaching a breakpoint. + Continue, +} + +/// Describes whether the current MIR instruction should be shown to the user. +enum InstructionVisibility { + NoInstruction, + Hidden, + Visible, +} + +/// Describes why execution stopped and returned control to the frontend. +pub(super) enum StepResult { + Step, + Breakpoint, +} + +fn normalize_path(path: PathBuf) -> PathBuf { + path.canonicalize().unwrap_or(path) +} + +impl<'tcx> PrirodaContext<'tcx> { + pub(super) fn new(ecx: MiriInterpCx<'tcx>) -> Self { + Self { ecx, breakpoints: HashMap::new(), current_location: None, last_location: None } + } + + pub(super) fn local_path(&self, location: &SourceLocation) -> Option { + let source_map = self.ecx.tcx.sess.source_map(); + location.local_path(source_map) + } + + fn current_source_position(&self) -> Option<(PathBuf, usize)> { + let location = self.current_location.as_ref()?; + Some((self.local_path(location)?, location.line)) + } + + // Used to treat `continue` like a source-level step for breakpoint checks: + // several MIR locations can point at one source line, but they should only + // report that source breakpoint once. + fn last_source_position(&self) -> Option<(PathBuf, usize)> { + let location = self.last_location.as_ref()?; + Some((self.local_path(location)?, location.line)) + } + + /// Step to the next visible MIR instruction. + fn stepi(&mut self) -> InterpResult<'tcx, StepResult> { + self.resume(ResumeMode::MirInstruction) + } + fn step(&mut self) -> InterpResult<'tcx, StepResult> { + self.resume(ResumeMode::SourceLine(self.current_source_position())) + } + + /// Continue execution until reaching a breakpoint or propagating termination. + fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { + self.resume(ResumeMode::Continue) + } + + fn set_breakpoint(&mut self, path: PathBuf, line: usize) -> BreakpointSetResult { + // FIXME: validate breakpoints here so every frontend gets the same behavior. + // Reject empty paths, missing files, directories, and line 0. Decide whether + // out-of-range lines should be rejected or kept as pending breakpoints. + // Report duplicate registrations separately. + + let path = normalize_path(path); + match self.breakpoints.entry(path.clone()).or_default().insert(line) { + true => BreakpointSetResult::Added(path, line), + false => BreakpointSetResult::Duplicate, + } + } + + /// Advance execution until the selected resume mode reaches a stopping point. + fn resume(&mut self, mode: ResumeMode) -> InterpResult<'tcx, StepResult> { + loop { + self.advance()?; + + // An explicit breakpoint should stop execution even when the current + // MIR instruction would normally be hidden during manual stepping. + if self.is_at_breakpoint() { + return interp_ok(StepResult::Breakpoint); + } + + match mode { + ResumeMode::MirInstruction + if matches!( + self.current_instruction_visibility(), + InstructionVisibility::Visible + ) => + { + return interp_ok(StepResult::Step); + } + + ResumeMode::SourceLine(ref prev_location) => { + match (prev_location, &self.current_location) { + // We started from an unmapped source location. Stop at the first mapped source location we can show to the user. + (None, Some(_)) => return interp_ok(StepResult::Step), + + (Some((prev_path, prev_line)), Some(current_location)) => { + if let Some(current_path) = self.local_path(current_location) { + // A source step stops when the visible source position changes to a different file or line. + if *prev_path != current_path || *prev_line != current_location.line + { + return interp_ok(StepResult::Step); + } + } + } + + _ => {} + } + } + + ResumeMode::MirInstruction | ResumeMode::Continue => {} + } + } + } + + /// Advance Miri by one interpreter-loop transition. + fn advance(&mut self) -> InterpResult<'tcx> { + // FIXME: use a Miri-owned scheduler-aware debugger step API before + // claiming support for multi-threaded interpreted programs. + + // State inspection should happen only after a successful step. + self.ecx.step_current_thread()?; + self.last_location = self.current_location.take(); + self.current_location = self.resolve_current_location(); + interp_ok(()) + } + + fn current_instruction_visibility(&self) -> InstructionVisibility { + // If the active thread has no stack frame, there is no MIR instruction to show. + let Some(frame) = self.ecx.active_thread_stack().last() else { + return InstructionVisibility::NoInstruction; + }; + + // `Right(span)` means the frame has source context but no precise MIR program-counter location. + let Either::Left(location) = frame.current_loc() else { + return InstructionVisibility::NoInstruction; + }; + + let basic_block = &frame.body().basic_blocks[location.block]; + + // `statement_index == statements.len()` points at the block terminator. + // Terminators affect control flow, so they are always visible. + let Some(statement) = basic_block.statements.get(location.statement_index) else { + return InstructionVisibility::Visible; + }; + + // Hide bookkeeping-only MIR statements during manual stepping. + match statement.kind { + mir::StatementKind::StorageLive(_) + | mir::StatementKind::StorageDead(_) + | mir::StatementKind::Nop => InstructionVisibility::Hidden, + _ => InstructionVisibility::Visible, + } + } + + fn is_at_breakpoint(&self) -> bool { + let Some(bp) = self.current_breakpoint() else { + return false; + }; + + // If the previous interpreter step had the same source position, this + // is another MIR location for the breakpoint we just reported. + self.last_source_position().as_ref() != Some(&bp) + } + + fn current_breakpoint(&self) -> Option<(PathBuf, usize)> { + let (path, line) = self.current_source_position()?; + let lines = self.breakpoints.get(&path)?; + + if lines.contains(&line) { Some((path, line)) } else { None } + } + + fn resolve_current_location(&self) -> Option { + // FIXME: resolve macro-backed lines such as `println!` and `assert_eq!` + // through `span.source_callsite()` before matching breakpoints. + let span = self.ecx.machine.current_user_relevant_span(); + if span.is_dummy() { + return None; + } + + let source_map = self.ecx.tcx.sess.source_map(); + let loc = source_map.lookup_char_pos(span.lo()); + + Some(SourceLocation { span, line: loc.line }) + } + + pub(super) fn run_command( + &mut self, + command: DebuggerCommand, + ) -> InterpResult<'tcx, CommandResult> { + match command { + DebuggerCommand::StepI => self.stepi().map(CommandResult::ExecutionStopped), + DebuggerCommand::Step => self.step().map(CommandResult::ExecutionStopped), + DebuggerCommand::Continue => + self.continue_execution().map(CommandResult::ExecutionStopped), + DebuggerCommand::Breakpoint(path, line) => + interp_ok(CommandResult::BreakpointResult(self.set_breakpoint(path, line))), + DebuggerCommand::ListLocals => interp_ok(CommandResult::Locals(self.list_locals())), + DebuggerCommand::Print(local) => + interp_ok(CommandResult::SingleLocal(self.get_local(local))), + DebuggerCommand::Follow(alloc_id, offset) => + self.follow_alloc(alloc_id, offset).map(CommandResult::Memory), + DebuggerCommand::TerminateSession => interp_ok(CommandResult::TerminateSession), + } + } + + fn follow_alloc(&self, alloc_id: AllocId, offset: usize) -> InterpResult<'tcx, String> { + let alloc = self.ecx.get_alloc_raw(alloc_id)?; + if offset > alloc.len() { + return Err(miri::err_unsup_format!( + "allocation offset {offset} is outside {alloc_id}" + )) + .into(); + } + + let memory = self.render_alloc_bytes(alloc_id, offset..alloc.len())?; + interp_ok(format!("Allocation {alloc_id}+{offset}: {memory}")) + } + + fn get_local(&self, local: usize) -> Option { + let frame = self.ecx.active_thread_stack().last()?; + + self.make_mir_local_desc(frame, local) + } + + /// Returns structured descriptions for locals in the innermost stack frame. + /// + /// Starts from all MIR locals, then enriches them with source names from + /// `var_debug_info` when a debug entry maps directly to a whole local. + fn list_locals(&self) -> Vec { + let Some(frame) = self.ecx.active_thread_stack().last() else { + return Vec::new(); + }; + + self.build_local_descs(frame) + } + + /// Renders the current byte range of an indirect MIR value. + /// + /// Initialized bytes are shown in hexadecimal, uninitialized bytes as `??`, + /// and complete pointer-sized provenance as pointer markers. + fn render_mplace_bytes(&self, mplace: &MPlaceTy<'tcx>) -> InterpResult<'tcx, String> { + let size = match self.ecx.size_and_align_of_val(mplace)? { + Some((size, _)) => size, + None => { + // Extern types cannot currently be executed as by-value locals, + // so this path cannot yet be covered by a Priroda UI fixture. + // FIXME: Add coverage once Priroda supports printing dereferenced places. + return interp_ok("".to_string()); + } + }; + + let size = size.bytes_usize(); + if size == 0 { + return interp_ok("[]".to_string()); + } + + let (alloc_id, offset, _) = + self.ecx.ptr_get_alloc_id(mplace.ptr(), size.try_into().unwrap())?; + let offset = offset.bytes_usize(); + let range = offset..offset.strict_add(size); + + self.render_alloc_bytes(alloc_id, range) + } + + /// Render a raw allocation range without requiring a typed memory place. + /// + /// This is also used by the future-facing `follow` command, where we have a + /// pointer target but do not yet know the target's type or size. + fn render_alloc_bytes( + &self, + alloc_id: AllocId, + range: Range, + ) -> InterpResult<'tcx, String> { + let alloc = self.ecx.get_alloc_raw(alloc_id)?; + + let mut rendered = Vec::with_capacity(range.len()); + + let ptr_size = self.ecx.tcx.data_layout.pointer_size(); + + for chunk in alloc.init_mask().range_as_init_chunks(range.into()) { + let chunk_range = chunk.range(); + let chunk_range = chunk_range.start.bytes_usize()..chunk_range.end.bytes_usize(); + + if chunk.is_init() { + let ptr_size = ptr_size.bytes_usize(); + let mut cursor = chunk_range.start; + + while cursor < chunk_range.end { + // Full pointer provenance is rendered as a pointer marker. Bytewise + // provenance fragments are intentionally left as raw bytes here: they do + // not represent a complete pointer-sized value. + if let Some(prov) = alloc.provenance().get_ptr(Size::from_bytes(cursor)) + && cursor + ptr_size <= chunk_range.end + { + let bytes = alloc.inspect_with_uninit_and_ptr_outside_interpreter( + cursor..cursor + ptr_size, + ); + let offset = read_target_uint(self.ecx.tcx.data_layout.endian, bytes) + .map_err(|err| { + miri::err_unsup_format!("invalid pointer representation: {err}") + })?; + + let offset = Size::from_bytes(offset); + rendered.push(format!("{:?}", Pointer::new(Some(prov), offset))); + + cursor += ptr_size; + } else { + let byte = alloc + .inspect_with_uninit_and_ptr_outside_interpreter(cursor..cursor + 1)[0]; + + rendered.push(format!("{byte:02x}")); + cursor += 1; + } + } + } else { + rendered.extend(std::iter::repeat_n("__".to_string(), chunk_range.len())); + } + } + + interp_ok(format!("[{}]", rendered.join(" "))) + } + + /// Render an evaluated operand using Rust-source-shaped containers with raw leaves. + /// + /// The operand is produced from live interpreter state, usually via `local_to_op` + /// for a whole MIR local or `eval_place_to_op` for a projected debug-info place. + /// + /// This intentionally does not call user `Debug` / `Display`, and it does not + /// try to make every scalar leaf pretty yet. Unsupported cases and leaf values + /// fall back to `render_op`, preserving the old raw byte/provenance renderer. + /// + /// FIXME: teach the leaf renderer about simple Rust scalars (`bool`, integers, + /// chars, raw pointers/references) once the source-shaped container output is + /// stable enough to stop depending on byte dumps for every field. + /// + /// FIXME: decide how much dereferencing belongs in this renderer. References + /// currently stay as raw pointer leaves; following them may belong in the + /// existing `follow` command instead of automatic local rendering. + fn render_source_shaped_op(&self, op: OpTy<'tcx>) -> String { + self.render_source_shaped_op_inner(op, 0) + } + + /// Recursive worker for `render_source_shaped_op`. + /// + /// The depth limit keeps cyclic/reference-heavy values from making debugger + /// output explode once more container kinds are added. At the limit, the raw + /// renderer remains the ground truth. + /// + /// FIXME: replace this fixed recursion limit with a value-size/output-budget + /// policy so large acyclic values and deeply nested values degrade more + /// predictably. + fn render_source_shaped_op_inner(&self, op: OpTy<'tcx>, depth: usize) -> String { + const MAX_SOURCE_SHAPE_DEPTH: usize = 8; + + if depth >= MAX_SOURCE_SHAPE_DEPTH { + return self.render_op(op); + } + + match op.layout.ty.kind() { + // Empty enums have no active variant to format. Unions do not record + // which field is currently active, so choosing one would be misleading. + // + // FIXME: support unions only with an explicit user-selected field or + // another source of active-field information. Guessing from layout + // bytes would make debugger output look more certain than it is. + ty::Adt(def, _) if def.variants().is_empty() || def.is_union() => self.render_op(op), + + ty::Adt(def, _) => { + // Enums need their runtime discriminant and a downcasted layout + // view before fields can be projected. Structs use their sole + // variant directly. Keep the display name tied to the same choice. + let (variant_idx, down, name) = if def.is_enum() { + let variant_idx = match self.ecx.read_discriminant(&op).discard_err() { + Some(variant_idx) => variant_idx, + // FIXME: expose this as an explicit render error when + // Priroda grows structured value states. Falling back to + // bytes keeps today's UI usable but hides why the enum + // could not be source-shaped. + None => return self.render_op(op), + }; + let down = match self.ecx.project_downcast(&op, variant_idx).discard_err() { + Some(down) => down, + // FIXME: distinguish invalid/uninitialized discriminants + // from projection bugs in the rendered output once locals + // can carry structured diagnostics. + None => return self.render_op(op), + }; + let variant_def = &def.variants()[variant_idx]; + ( + variant_idx, + down, + format!("{}::{}", self.ecx.tcx.item_name(def.did()), variant_def.name), + ) + } else { + let variant_idx = FIRST_VARIANT; + let variant_def = &def.variants()[variant_idx]; + (variant_idx, op.clone(), variant_def.name.to_string()) + }; + + let variant_def = &def.variants()[variant_idx]; + + let mut fields = Vec::with_capacity(variant_def.fields.len()); + for i in 0..variant_def.fields.len() { + let field_idx = FieldIdx::from_usize(i); + // `project_field` avoids manual offset math and works for both + // immediate and memory-backed operands through `Projectable`. + let field_op = match self.ecx.project_field(&down, field_idx).discard_err() { + Some(field_op) => field_op, + // FIXME: preserve the successfully rendered fields and + // mark only this field as unavailable once the value model + // can represent partial render failures. + None => return self.render_op(op), + }; + fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); + } + + // Match Rust constructor spelling: + // - `Const`: unit structs/variants, e.g. `UnitStruct`, `Enum::Unit` + // - `Fn`: tuple structs/variants, e.g. `Pair(a, b)` or `EmptyTuple()` + // - `None`: braced structs/variants, including the empty `{}` case + match variant_def.ctor_kind() { + Some(CtorKind::Const) => name, + Some(CtorKind::Fn) => format!("{name}({})", fields.join(", ")), + None if fields.is_empty() => format!("{name} {{}}"), + None => { + let fields = variant_def + .fields + .iter() + .zip(fields) + .map(|(field_def, value)| format!("{}: {value}", field_def.name)) + .collect::>() + .join(", "); + format!("{name} {{ {fields} }}") + } + } + } + + ty::Tuple(args) => { + let mut fields = Vec::with_capacity(args.len()); + for i in 0..args.len() { + // Tuples have no field names in source, so preserve their + // source field order and render children positionally. + let field_op = + match self.ecx.project_field(&op, FieldIdx::from_usize(i)).discard_err() { + Some(field_op) => field_op, + // FIXME: render tuple fields independently so one + // projection failure does not throw away the whole + // source-shaped tuple. + None => return self.render_op(op), + }; + fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); + } + + if fields.len() == 1 { + format!("({},)", fields[0]) + } else { + format!("({})", fields.join(", ")) + } + } + + ty::Array(_, _) | ty::Slice(_) => { + // `project_array_fields` uses the dynamic length for slices. That + // avoids the classic mistake of treating slice layout as a fixed + // zero-length array. + let mut iter = match self.ecx.project_array_fields(&op).discard_err() { + Some(iter) => iter, + // FIXME: when slice metadata is invalid, show that as a slice + // length problem instead of silently falling back to raw bytes. + None => return self.render_op(op), + }; + + let mut fields = Vec::new(); + // FIXME: add an output budget/truncation policy before rendering + // very large arrays or slices in full. + loop { + match iter.next(&self.ecx).discard_err() { + Some(Some((_idx, field_op))) => + fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)), + Some(None) => break, + // FIXME: keep already-rendered elements and mark the + // failed index once partial render errors are supported. + None => return self.render_op(op), + } + } + + format!("[{}]", fields.join(", ")) + } + + // FIXME: consider source-shaped special cases for strings, closures, + // generators/coroutines, trait objects, and SIMD/vector-like types. + // Until then these stay on the raw renderer path. + _ => self.render_op(op), + } + } + + /// Render an evaluated operand using the same raw representation for + /// whole locals and projected MIR places. + fn render_op(&self, op: OpTy<'tcx>) -> String { + match op.as_mplace_or_imm() { + Either::Right(imm) => format!("{imm}"), + + Either::Left(mplace) => + match self.render_mplace_bytes(&mplace).report_err() { + Ok(bytes) => bytes, + Err(err) => format!("", interpret::format_interp_error(err)), + }, + } + } + + /// Render the source-side path from composite debug info, such as `.field`. + fn render_source_projection( + fragment: Option<&VarDebugInfoFragment<'tcx>>, + ) -> Option> { + let VarDebugInfoFragment { ty, projection } = fragment?; + + // Walk the source-side projection from the original + // composite variable type. Each `Field` element stores the + // resulting field type, so resolve the field name from the + // current base type before advancing to `field_ty`. + let mut projection_ty = ty; + + Some( + projection + .iter() + .map(|elem| { + match elem { + ProjectionElem::Field(field_idx, field_ty) => { + let rendered = match projection_ty.kind() { + TyKind::Adt(adt_def, _args) if adt_def.is_struct() => { + let variant = adt_def.non_enum_variant(); + let field = &variant.fields[*field_idx]; + Symbol::intern(&format!(".{}", field.name)) + } + + TyKind::Tuple(_) => + Symbol::intern(&format!(".{}", field_idx.index())), + + _ => Symbol::intern("."), + }; + + projection_ty = field_ty; + + rendered + } + // `VarDebugInfoFragment::projection` is expected to be + // field-only. If that ever changes, keep the unexpected + // segment visible instead of silently rendering a + // misleading source path. + other => Symbol::intern(&format!(".")), + } + }) + .collect(), + ) + } + + /// Render the MIR storage-side path that backs a debug-info local. + fn render_storage_projection(projection: &[mir::PlaceElem<'tcx>]) -> Vec { + projection + .iter() + .map(|projection_elem| { + match projection_elem { + ProjectionElem::Field(field_idx, _) => StorageProj::Field(field_idx.index()), + ProjectionElem::Deref => StorageProj::Deref, + ProjectionElem::Downcast(Some(name), _) => StorageProj::Downcast(*name), + ProjectionElem::Downcast(None, variant_idx) => + StorageProj::Variant(variant_idx.index()), + other => StorageProj::Unsupported(format!("{other:?}")), + } + }) + .collect() + } + + /// Builds the baseline debugger row for one MIR local without scanning debug info. + fn make_mir_local_desc( + &self, + frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, + local: usize, + ) -> Option { + let local = mir::Local::from_usize(local); + let local_decl = frame.body().local_decls.get(local)?; + + // Create LocalDesc for MIR local before processing debug info. + // Debug-info enrichment is layered on by build_local_descs. + let mut local_desc = LocalDesc { + source_name: None, + source_projection: None, + local: Some(local), + storage_projection: Vec::new(), + ty: local_decl.ty.to_string(), + value: "".to_string(), + }; + + match &frame.locals[local].as_mplace_or_imm() { + None => { + local_desc.value = "".to_string(); + } + Some(Either::Right(Uninit)) => local_desc.value = "".to_string(), + + Some(Either::Left(_) | Either::Right(_)) => { + let op = self + .ecx + .local_to_op(local, None) + .expect("this error can only occur in CTFE on generic code"); + local_desc.value = self.render_source_shaped_op(op); + } + }; + + Some(local_desc) + } + + fn build_local_descs( + &self, + frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, + ) -> Vec { + let local_decls = &frame.body().local_decls; + + let mut local_descs: Vec = Vec::with_capacity(local_decls.len()); + + // Start with one baseline row for every MIR local, then layer debug info on top. + for (local_idx, _) in local_decls.iter_enumerated() { + local_descs.push(self.make_mir_local_desc(frame, local_idx.index()).unwrap()); + } + + // FIXME: Finish classifying `var_debug_info` by keeping the source path + // and MIR storage path separate: + // + // - source side: `var_debug_info.name` plus + // `var_debug_info.composite.projection` + // - storage side: `VarDebugInfoContents::Place(place).local` plus + // `place.projection` + // + // Already handled by the `place.as_local()` path below: + // - whole source variable -> whole MIR local: + // `composite = None`, `Place(_N)` with empty projection. + // - source fragment -> whole MIR local: + // `composite = Some(source_proj)`, `Place(_N)` with empty projection. + // + // Remaining cases to represent or explicitly defer: + // - whole source variable -> projected MIR storage: + // `composite = None`, `Place(_N.proj)`. + // - source fragment -> projected MIR storage: + // `composite = Some(source_proj)`, `Place(_N.storage_proj)`. + // - source variable/fragment -> constant: + // `Const(...)`, with no MIR local id. + // - optimized-out/debug-only/unsupported shapes: + // explicit deferred state, not silent discard. + // + // Final output should be produced by walking `Vec`, + // then append explicit deferred/debug-info-only rows where needed. + // Related: SROA can split a source local like `_slice: ExtraSlice` into + // field locals whose debug paths should be printed as `_slice._slice` + // and `_slice._extra`, not as two separate locals both named `_slice`. + + // Whole-place debug entries enrich the direct storage-local description. + // Projected places are evaluated from their original MIR Place and use + // the same raw renderer as ordinary locals. + for var_debug_info in &frame.body().var_debug_info { + if let VarDebugInfoContents::Place(place) = &var_debug_info.value { + if let Some(local_idx) = place.as_local() + && local_descs[local_idx.index()].source_name.is_none() + { + let local_idx = local_idx.index(); + local_descs[local_idx].source_projection = + Self::render_source_projection(var_debug_info.composite.as_deref()); + local_descs[local_idx].source_name = Some(var_debug_info.name); + } else if !place.projection.is_empty() { + let storage_projection = Self::render_storage_projection(place.projection); + let source_projection = + Self::render_source_projection(var_debug_info.composite.as_deref()); + let value = self + .ecx + .eval_place_to_op(*place, None) + .map(|op| self.render_source_shaped_op(op)) + .unwrap_or_else(|err| { + format!("", interpret::format_interp_error(err)) + }); + + local_descs.push(LocalDesc { + source_name: Some(var_debug_info.name), + source_projection, + local: Some(place.local), + storage_projection, + ty: place.ty(local_decls, self.ecx.tcx.tcx).ty.to_string(), + value, + }); + } + } + } + + local_descs + } +} + +pub(super) enum DebuggerCommand { + StepI, + Step, + TerminateSession, + Continue, + Breakpoint(PathBuf, usize), + ListLocals, + Print(usize), + Follow(AllocId, usize), +} + +pub(super) enum BreakpointSetResult { + Added(PathBuf, usize), + Duplicate, + // FIXME: add pending breakpoint support later if needed. +} + +pub(super) enum CommandResult { + ExecutionStopped(StepResult), + BreakpointResult(BreakpointSetResult), + Locals(Vec), + SingleLocal(Option), + Memory(String), + // FIXME: distinguish terminating the debugger session from disconnecting a + // frontend and terminating the interpreted program once multiple frontends exist. + TerminateSession, +} diff --git a/src/tools/miri/priroda/src/frontend/cli.rs b/src/tools/miri/priroda/src/frontend/cli.rs new file mode 100644 index 0000000000000..e4e92351a92f6 --- /dev/null +++ b/src/tools/miri/priroda/src/frontend/cli.rs @@ -0,0 +1,176 @@ +use std::io::{self, Write}; +use std::num::NonZeroU64; +use std::path::PathBuf; + +use miri::{InterpResult, interp_ok}; +use rustc_middle::mir::interpret::AllocId; + +use crate::debugger::{ + BreakpointSetResult, CommandResult, DebuggerCommand, PrirodaContext, StepResult, +}; + +pub(crate) struct Cli; + +impl Cli { + pub(crate) fn run_cli_loop<'tcx>( + &self, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx> { + loop { + print!("(priroda) "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + let bytes_read = io::stdin().read_line(&mut input).unwrap(); + + if bytes_read == 0 { + println!("stdin closed, stopping"); + return interp_ok(()); + } + + if let Some(command) = self.parse_command(&input) { + let command_res = session.run_command(command)?; + if !Self::print_command_result(command_res, session)? { + return interp_ok(()); + }; + } else { + println!("no command"); + } + + io::stdout().flush().unwrap(); + } + } + + fn print_command_result<'tcx>( + command_res: CommandResult, + session: &PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, bool> { + match command_res { + CommandResult::ExecutionStopped(result) => { + if matches!(result, StepResult::Breakpoint) { + println!("Hit breakpoint"); + } + Self::print_location(session); + } + CommandResult::BreakpointResult(res) => + match res { + BreakpointSetResult::Added(path, line) => { + println!("breakpoint added: {}:{}", path.display(), line) + } + + BreakpointSetResult::Duplicate => println!("Duplicate breakpoint"), + }, + CommandResult::Locals(locals_desc) => + if locals_desc.is_empty() { + println!("no locals"); + } else { + for local_desc in &locals_desc { + let source_projection = local_desc.source_projection_str(); + + let name = local_desc + .source_name + .map_or_else(|| "".to_string(), |name| name.to_string()); + + let display_name = format!("{name}{source_projection}"); + + let local_id = local_desc.local.map_or_else( + || "".to_string(), + |local_idx| format!("_{}", local_idx.index()), + ); + + let display_local_id = + format!("{}{}", local_id, local_desc.storage_projection_str()); + println!( + "Name: {}, Id: {}, Ty: {}, Value: {}", + display_name, display_local_id, local_desc.ty, local_desc.value + ); + } + }, + CommandResult::SingleLocal(local_desc) => + match local_desc { + Some(local_desc) => { + println!( + "Id: _{}, Ty: {}, Value: {}", + local_desc.local.unwrap().index(), + local_desc.ty, + local_desc.value + ); + } + None => println!("no local for this id"), + }, + CommandResult::Memory(memory) => println!("{memory}"), + CommandResult::TerminateSession => { + println!("quitting"); + return interp_ok(false); + } + } + interp_ok(true) + } + + fn parse_command(&self, input: &str) -> Option { + // TODO: look at the Spanned crate for how to easily produce errors in + // rustc's style while manually parsing text input. + // FIXME: we need to distinguish malformed input from the unknown commands by returning useful + // command error that describes if it malformed or non exist command + let input = input.trim(); + let mut parts = input.splitn(2, char::is_whitespace); + let command = parts.next().unwrap_or(""); + let args = parts.next().unwrap_or("").trim(); + + match command { + // FIXME: empty line should repats last command user typed not exeute specific command. + "" | "si" | "stepi" => Some(DebuggerCommand::StepI), + "s" | "step" => Some(DebuggerCommand::Step), + "q" | "quit" => Some(DebuggerCommand::TerminateSession), + "c" | "continue" => Some(DebuggerCommand::Continue), + "b" | "break" => self.parse_breakpoint(args), + "l" | "locals" => Some(DebuggerCommand::ListLocals), + "p" | "print" => self.parse_print_local(args), + "f" | "follow" => self.parse_follow(args), + _ => None, + } + } + + fn print_location<'tcx>(session: &PrirodaContext<'tcx>) { + match &session.current_location { + Some(location) => + if let Some(path) = session.local_path(location) { + println!("{}:{}", path.display(), location.line); + } else { + let source_map = session.ecx.tcx.sess.source_map(); + println!("{}", source_map.span_to_diagnostic_string(location.span)); + }, + None => println!("no-location"), + } + io::stdout().flush().unwrap(); + } + + fn parse_breakpoint(&self, input: &str) -> Option { + // FIXME: return a typed CommandError so malformed breakpoint input is + // distinguishable from an unknown command. Semantic validation belongs + // in PrirodaContext::set_breakpoint so non-CLI frontends cannot bypass it. + let (path, line) = input.rsplit_once(':')?; + let line = line.parse().ok()?; + + Some(DebuggerCommand::Breakpoint(PathBuf::from(path), line)) + } + + fn parse_print_local(&self, input: &str) -> Option { + let local = input.parse().ok()?; + Some(DebuggerCommand::Print(local)) + } + + fn parse_follow(&self, input: &str) -> Option { + let mut parts = input.split_whitespace(); + let alloc_id = parts.next()?; + let offset = parts.next()?; + if parts.next().is_some() { + return None; + } + + let alloc_id = alloc_id.strip_prefix("alloc").unwrap_or(alloc_id).parse().ok()?; + let alloc_id = AllocId(NonZeroU64::new(alloc_id)?); + let offset = offset.parse().ok()?; + Some(DebuggerCommand::Follow(alloc_id, offset)) + } +} diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs new file mode 100644 index 0000000000000..75536910aacac --- /dev/null +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -0,0 +1,17 @@ +use miri::{InterpResult, interp_ok}; + +use crate::debugger::PrirodaContext; + +/// Debug Adapter Protocol frontend. +pub(crate) struct Dap; + +impl Dap { + /// Serve DAP requests on stdin/stdout. + pub(crate) fn run_dap_loop<'tcx>( + &self, + _session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx> { + // FIXME: implement DAP framing and request dispatch on top of PrirodaContext. + interp_ok(()) + } +} diff --git a/src/tools/miri/priroda/src/frontend/mod.rs b/src/tools/miri/priroda/src/frontend/mod.rs new file mode 100644 index 0000000000000..8d2f57fb674f8 --- /dev/null +++ b/src/tools/miri/priroda/src/frontend/mod.rs @@ -0,0 +1,5 @@ +mod cli; +mod dap; + +pub(super) use cli::Cli; +pub(super) use dap::Dap; diff --git a/src/tools/miri/priroda/src/main.rs b/src/tools/miri/priroda/src/main.rs index bb2327c8338e0..9b0efdf9fadb8 100644 --- a/src/tools/miri/priroda/src/main.rs +++ b/src/tools/miri/priroda/src/main.rs @@ -15,26 +15,17 @@ extern crate rustc_session; extern crate rustc_span; extern crate rustc_type_ir; -use std::collections::{HashMap, HashSet}; -use std::io::{self, Write}; -use std::num::NonZeroU64; -use std::ops::Range; -use std::path::PathBuf; +mod debugger; +mod frontend; -use miri::Immediate::Uninit; +use debugger::PrirodaContext; use miri::*; -use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; use rustc_driver::Compilation; use rustc_hir::attrs::CrateType; -use rustc_hir::def::CtorKind; use rustc_interface::interface; -use rustc_middle::mir::interpret::AllocId; -use rustc_middle::mir::{self, Local, ProjectionElem, VarDebugInfoContents, VarDebugInfoFragment}; -use rustc_middle::ty::{self, TyCtxt, TyKind}; +use rustc_middle::ty::TyCtxt; use rustc_session::EarlyDiagCtxt; use rustc_session::config::ErrorOutputType; -use rustc_span::source_map::SourceMap; -use rustc_span::{Span, Symbol}; fn find_sysroot() -> String { std::env::var("MIRI_SYSROOT") @@ -115,14 +106,8 @@ impl rustc_driver::Callbacks for PrirodaCompilerCalls { let mut session = PrirodaContext::new(ecx); let result = match self.frontend { - Frontend::Cli => { - let cli = Cli {}; - cli.run_cli_loop(&mut session) - } - Frontend::Dap => { - let dap = Dap {}; - dap.run_dap_loop(&mut session) - } + Frontend::Cli => frontend::Cli {}.run_cli_loop(&mut session), + Frontend::Dap => frontend::Dap {}.run_dap_loop(&mut session), }; match result.report_err() { @@ -152,977 +137,3 @@ fn create_ecx<'tcx>(tcx: TyCtxt<'tcx>) -> MiriInterpCx<'tcx> { // FIXME: report interpreter initialization failures instead of panicking. miri::create_ecx(tcx, entry_id, entry_type, &config, None).unwrap() } - -/// Structured source information for frontends. -struct SourceLocation { - // storing `span` to use it lazily to compute path. - span: Span, - line: usize, -} - -impl SourceLocation { - fn local_path(&self, source_map: &SourceMap) -> Option { - let loc = source_map.lookup_char_pos(self.span.lo()); - loc.file.name.clone().into_local_path().map(normalize_path) - } -} - -/// Source-level breakpoints indexed by normalized path, then line. -type BreakpointTable = HashMap>; - -/// Owns one interpreter session and its debugger state. -/// -/// Frontend rendering should eventually live outside this type. -struct PrirodaContext<'tcx> { - ecx: MiriInterpCx<'tcx>, - breakpoints: BreakpointTable, - current_location: Option, - last_location: Option, -} - -enum StorageProj { - Field(usize), - Deref, - Downcast(Symbol), - Variant(usize), - Unsupported(String), -} - -impl StorageProj { - fn render(&self) -> String { - match self { - StorageProj::Field(field_idx) => format!(".{field_idx}"), - StorageProj::Deref => format!(".*"), - StorageProj::Downcast(name) => format!(" as {name}"), - StorageProj::Variant(variant_idx) => format!(" as variant#{variant_idx}"), - StorageProj::Unsupported(unsop) => format!("."), - } - } -} - -struct LocalDesc { - /// Source variable name from `VarDebugInfo`, if this row has one. - source_name: Option, - - /// Source-side projection from `VarDebugInfo::composite`, e.g. `.field` in source fragment `x.field`. - source_projection: Option>, - - /// MIR storage local that backs this description, if any. - local: Option, - - /// rendered/debug MIR place projection for now - storage_projection: Vec, - - /// Display-rendered type for this description. - ty: String, - - /// Run-time state for now; will be expanded later - value: String, -} - -/// Controls when execution returns to the frontend. -enum ResumeMode { - /// Stop at the next visible MIR instruction. - MirInstruction, - /// Stop at the next source line - /// - /// Take `Option` because some cases current state has no mapped to source code location - SourceLine(Option<(PathBuf, usize)>), - /// Continue until reaching a breakpoint. - Continue, -} - -/// Describes whether the current MIR instruction should be shown to the user. -enum InstructionVisibility { - NoInstruction, - Hidden, - Visible, -} - -/// Describes why execution stopped and returned control to the frontend. -enum StepResult { - Step, - Breakpoint, -} - -fn normalize_path(path: PathBuf) -> PathBuf { - path.canonicalize().unwrap_or(path) -} - -impl<'tcx> PrirodaContext<'tcx> { - fn new(ecx: MiriInterpCx<'tcx>) -> Self { - Self { ecx, breakpoints: HashMap::new(), current_location: None, last_location: None } - } - - fn local_path(&self, location: &SourceLocation) -> Option { - let source_map = self.ecx.tcx.sess.source_map(); - location.local_path(source_map) - } - - fn current_source_position(&self) -> Option<(PathBuf, usize)> { - let location = self.current_location.as_ref()?; - Some((self.local_path(location)?, location.line)) - } - - // Used to treat `continue` like a source-level step for breakpoint checks: - // several MIR locations can point at one source line, but they should only - // report that source breakpoint once. - fn last_source_position(&self) -> Option<(PathBuf, usize)> { - let location = self.last_location.as_ref()?; - Some((self.local_path(location)?, location.line)) - } - - /// Step to the next visible MIR instruction. - fn stepi(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::MirInstruction) - } - fn step(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::SourceLine(self.current_source_position())) - } - - /// Continue execution until reaching a breakpoint or propagating termination. - fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::Continue) - } - - fn set_breakpoint(&mut self, path: PathBuf, line: usize) -> BreakpointSetResult { - // FIXME: validate breakpoints here so every frontend gets the same behavior. - // Reject empty paths, missing files, directories, and line 0. Decide whether - // out-of-range lines should be rejected or kept as pending breakpoints. - // Report duplicate registrations separately. - - let path = normalize_path(path); - match self.breakpoints.entry(path.clone()).or_default().insert(line) { - true => BreakpointSetResult::Added(path, line), - false => BreakpointSetResult::Duplicate, - } - } - - /// Advance execution until the selected resume mode reaches a stopping point. - fn resume(&mut self, mode: ResumeMode) -> InterpResult<'tcx, StepResult> { - loop { - self.advance()?; - - // An explicit breakpoint should stop execution even when the current - // MIR instruction would normally be hidden during manual stepping. - if self.is_at_breakpoint() { - return interp_ok(StepResult::Breakpoint); - } - - match mode { - ResumeMode::MirInstruction - if matches!( - self.current_instruction_visibility(), - InstructionVisibility::Visible - ) => - { - return interp_ok(StepResult::Step); - } - - ResumeMode::SourceLine(ref prev_location) => { - match (prev_location, &self.current_location) { - // We started from an unmapped source location. Stop at the first mapped source location we can show to the user. - (None, Some(_)) => return interp_ok(StepResult::Step), - - (Some((prev_path, prev_line)), Some(current_location)) => { - if let Some(current_path) = self.local_path(current_location) { - // A source step stops when the visible source position changes to a different file or line. - if *prev_path != current_path || *prev_line != current_location.line - { - return interp_ok(StepResult::Step); - } - } - } - - _ => {} - } - } - - ResumeMode::MirInstruction | ResumeMode::Continue => {} - } - } - } - - /// Advance Miri by one interpreter-loop transition. - fn advance(&mut self) -> InterpResult<'tcx> { - // FIXME: use a Miri-owned scheduler-aware debugger step API before - // claiming support for multi-threaded interpreted programs. - - // State inspection should happen only after a successful step. - self.ecx.step_current_thread()?; - self.last_location = self.current_location.take(); - self.current_location = self.resolve_current_location(); - interp_ok(()) - } - - fn current_instruction_visibility(&self) -> InstructionVisibility { - // If the active thread has no stack frame, there is no MIR instruction to show. - let Some(frame) = self.ecx.active_thread_stack().last() else { - return InstructionVisibility::NoInstruction; - }; - - // `Right(span)` means the frame has source context but no precise MIR program-counter location. - let Either::Left(location) = frame.current_loc() else { - return InstructionVisibility::NoInstruction; - }; - - let basic_block = &frame.body().basic_blocks[location.block]; - - // `statement_index == statements.len()` points at the block terminator. - // Terminators affect control flow, so they are always visible. - let Some(statement) = basic_block.statements.get(location.statement_index) else { - return InstructionVisibility::Visible; - }; - - // Hide bookkeeping-only MIR statements during manual stepping. - match statement.kind { - mir::StatementKind::StorageLive(_) - | mir::StatementKind::StorageDead(_) - | mir::StatementKind::Nop => InstructionVisibility::Hidden, - _ => InstructionVisibility::Visible, - } - } - - fn is_at_breakpoint(&self) -> bool { - let Some(bp) = self.current_breakpoint() else { - return false; - }; - - // If the previous interpreter step had the same source position, this - // is another MIR location for the breakpoint we just reported. - self.last_source_position().as_ref() != Some(&bp) - } - - fn current_breakpoint(&self) -> Option<(PathBuf, usize)> { - let (path, line) = self.current_source_position()?; - let lines = self.breakpoints.get(&path)?; - - if lines.contains(&line) { Some((path, line)) } else { None } - } - - fn resolve_current_location(&self) -> Option { - // FIXME: resolve macro-backed lines such as `println!` and `assert_eq!` - // through `span.source_callsite()` before matching breakpoints. - let span = self.ecx.machine.current_user_relevant_span(); - if span.is_dummy() { - return None; - } - - let source_map = self.ecx.tcx.sess.source_map(); - let loc = source_map.lookup_char_pos(span.lo()); - - Some(SourceLocation { span, line: loc.line }) - } - - fn run_command(&mut self, command: DebuggerCommand) -> InterpResult<'tcx, CommandResult> { - match command { - DebuggerCommand::StepI => self.stepi().map(CommandResult::ExecutionStopped), - DebuggerCommand::Step => self.step().map(CommandResult::ExecutionStopped), - DebuggerCommand::Continue => - self.continue_execution().map(CommandResult::ExecutionStopped), - DebuggerCommand::Breakpoint(path, line) => - interp_ok(CommandResult::BreakpointResult(self.set_breakpoint(path, line))), - DebuggerCommand::ListLocals => interp_ok(CommandResult::Locals(self.list_locals())), - DebuggerCommand::Print(local) => - interp_ok(CommandResult::SingleLocal(self.get_local(local))), - DebuggerCommand::Follow(alloc_id, offset) => - self.follow_alloc(alloc_id, offset).map(CommandResult::Memory), - DebuggerCommand::TerminateSession => interp_ok(CommandResult::TerminateSession), - } - } - - fn follow_alloc(&self, alloc_id: AllocId, offset: usize) -> InterpResult<'tcx, String> { - let alloc = self.ecx.get_alloc_raw(alloc_id)?; - if offset > alloc.len() { - return Err(miri::err_unsup_format!( - "allocation offset {offset} is outside {alloc_id}" - )) - .into(); - } - - let memory = self.render_alloc_bytes(alloc_id, offset..alloc.len())?; - interp_ok(format!("Allocation {alloc_id}+{offset}: {memory}")) - } - - fn get_local(&self, local: usize) -> Option { - let frame = self.ecx.active_thread_stack().last()?; - - self.make_mir_local_desc(frame, local) - } - - /// Returns structured descriptions for locals in the innermost stack frame. - /// - /// Starts from all MIR locals, then enriches them with source names from - /// `var_debug_info` when a debug entry maps directly to a whole local. - fn list_locals(&self) -> Vec { - let Some(frame) = self.ecx.active_thread_stack().last() else { - return Vec::new(); - }; - - self.build_local_descs(frame) - } - - /// Renders the current byte range of an indirect MIR value. - /// - /// Initialized bytes are shown in hexadecimal, uninitialized bytes as `??`, - /// and complete pointer-sized provenance as pointer markers. - fn render_mplace_bytes(&self, mplace: &MPlaceTy<'tcx>) -> InterpResult<'tcx, String> { - let size = match self.ecx.size_and_align_of_val(mplace)? { - Some((size, _)) => size, - None => { - // Extern types cannot currently be executed as by-value locals, - // so this path cannot yet be covered by a Priroda UI fixture. - // FIXME: Add coverage once Priroda supports printing dereferenced places. - return interp_ok("".to_string()); - } - }; - - let size = size.bytes_usize(); - if size == 0 { - return interp_ok("[]".to_string()); - } - - let (alloc_id, offset, _) = - self.ecx.ptr_get_alloc_id(mplace.ptr(), size.try_into().unwrap())?; - let offset = offset.bytes_usize(); - let range = offset..offset.strict_add(size); - - self.render_alloc_bytes(alloc_id, range) - } - - /// Render a raw allocation range without requiring a typed memory place. - /// - /// This is also used by the future-facing `follow` command, where we have a - /// pointer target but do not yet know the target's type or size. - fn render_alloc_bytes( - &self, - alloc_id: AllocId, - range: Range, - ) -> InterpResult<'tcx, String> { - let alloc = self.ecx.get_alloc_raw(alloc_id)?; - - let mut rendered = Vec::with_capacity(range.len()); - - let ptr_size = self.ecx.tcx.data_layout.pointer_size(); - - for chunk in alloc.init_mask().range_as_init_chunks(range.into()) { - let chunk_range = chunk.range(); - let chunk_range = chunk_range.start.bytes_usize()..chunk_range.end.bytes_usize(); - - if chunk.is_init() { - let ptr_size = ptr_size.bytes_usize(); - let mut cursor = chunk_range.start; - - while cursor < chunk_range.end { - // Full pointer provenance is rendered as a pointer marker. Bytewise - // provenance fragments are intentionally left as raw bytes here: they do - // not represent a complete pointer-sized value. - if let Some(prov) = alloc.provenance().get_ptr(Size::from_bytes(cursor)) - && cursor + ptr_size <= chunk_range.end - { - let bytes = alloc.inspect_with_uninit_and_ptr_outside_interpreter( - cursor..cursor + ptr_size, - ); - let offset = read_target_uint(self.ecx.tcx.data_layout.endian, bytes) - .map_err(|err| { - miri::err_unsup_format!("invalid pointer representation: {err}") - })?; - - let offset = Size::from_bytes(offset); - rendered.push(format!("{:?}", Pointer::new(Some(prov), offset))); - - cursor += ptr_size; - } else { - let byte = alloc - .inspect_with_uninit_and_ptr_outside_interpreter(cursor..cursor + 1)[0]; - - rendered.push(format!("{byte:02x}")); - cursor += 1; - } - } - } else { - rendered.extend(std::iter::repeat_n("__".to_string(), chunk_range.len())); - } - } - - interp_ok(format!("[{}]", rendered.join(" "))) - } - - /// Render an evaluated operand using Rust-source-shaped containers with raw leaves. - /// - /// The operand is produced from live interpreter state, usually via `local_to_op` - /// for a whole MIR local or `eval_place_to_op` for a projected debug-info place. - /// - /// This intentionally does not call user `Debug` / `Display`, and it does not - /// try to make every scalar leaf pretty yet. Unsupported cases and leaf values - /// fall back to `render_op`, preserving the old raw byte/provenance renderer. - /// - /// FIXME: teach the leaf renderer about simple Rust scalars (`bool`, integers, - /// chars, raw pointers/references) once the source-shaped container output is - /// stable enough to stop depending on byte dumps for every field. - /// - /// FIXME: decide how much dereferencing belongs in this renderer. References - /// currently stay as raw pointer leaves; following them may belong in the - /// existing `follow` command instead of automatic local rendering. - fn render_source_shaped_op(&self, op: OpTy<'tcx>) -> String { - self.render_source_shaped_op_inner(op, 0) - } - - /// Recursive worker for `render_source_shaped_op`. - /// - /// The depth limit keeps cyclic/reference-heavy values from making debugger - /// output explode once more container kinds are added. At the limit, the raw - /// renderer remains the ground truth. - /// - /// FIXME: replace this fixed recursion limit with a value-size/output-budget - /// policy so large acyclic values and deeply nested values degrade more - /// predictably. - fn render_source_shaped_op_inner(&self, op: OpTy<'tcx>, depth: usize) -> String { - const MAX_SOURCE_SHAPE_DEPTH: usize = 8; - - if depth >= MAX_SOURCE_SHAPE_DEPTH { - return self.render_op(op); - } - - match op.layout.ty.kind() { - // Empty enums have no active variant to format. Unions do not record - // which field is currently active, so choosing one would be misleading. - // - // FIXME: support unions only with an explicit user-selected field or - // another source of active-field information. Guessing from layout - // bytes would make debugger output look more certain than it is. - ty::Adt(def, _) if def.variants().is_empty() || def.is_union() => self.render_op(op), - - ty::Adt(def, _) => { - // Enums need their runtime discriminant and a downcasted layout - // view before fields can be projected. Structs use their sole - // variant directly. Keep the display name tied to the same choice. - let (variant_idx, down, name) = if def.is_enum() { - let variant_idx = match self.ecx.read_discriminant(&op).discard_err() { - Some(variant_idx) => variant_idx, - // FIXME: expose this as an explicit render error when - // Priroda grows structured value states. Falling back to - // bytes keeps today's UI usable but hides why the enum - // could not be source-shaped. - None => return self.render_op(op), - }; - let down = match self.ecx.project_downcast(&op, variant_idx).discard_err() { - Some(down) => down, - // FIXME: distinguish invalid/uninitialized discriminants - // from projection bugs in the rendered output once locals - // can carry structured diagnostics. - None => return self.render_op(op), - }; - let variant_def = &def.variants()[variant_idx]; - ( - variant_idx, - down, - format!("{}::{}", self.ecx.tcx.item_name(def.did()), variant_def.name), - ) - } else { - let variant_idx = FIRST_VARIANT; - let variant_def = &def.variants()[variant_idx]; - (variant_idx, op.clone(), variant_def.name.to_string()) - }; - - let variant_def = &def.variants()[variant_idx]; - - let mut fields = Vec::with_capacity(variant_def.fields.len()); - for i in 0..variant_def.fields.len() { - let field_idx = FieldIdx::from_usize(i); - // `project_field` avoids manual offset math and works for both - // immediate and memory-backed operands through `Projectable`. - let field_op = match self.ecx.project_field(&down, field_idx).discard_err() { - Some(field_op) => field_op, - // FIXME: preserve the successfully rendered fields and - // mark only this field as unavailable once the value model - // can represent partial render failures. - None => return self.render_op(op), - }; - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); - } - - // Match Rust constructor spelling: - // - `Const`: unit structs/variants, e.g. `UnitStruct`, `Enum::Unit` - // - `Fn`: tuple structs/variants, e.g. `Pair(a, b)` or `EmptyTuple()` - // - `None`: braced structs/variants, including the empty `{}` case - match variant_def.ctor_kind() { - Some(CtorKind::Const) => name, - Some(CtorKind::Fn) => format!("{name}({})", fields.join(", ")), - None if fields.is_empty() => format!("{name} {{}}"), - None => { - let fields = variant_def - .fields - .iter() - .zip(fields) - .map(|(field_def, value)| format!("{}: {value}", field_def.name)) - .collect::>() - .join(", "); - format!("{name} {{ {fields} }}") - } - } - } - - ty::Tuple(args) => { - let mut fields = Vec::with_capacity(args.len()); - for i in 0..args.len() { - // Tuples have no field names in source, so preserve their - // source field order and render children positionally. - let field_op = - match self.ecx.project_field(&op, FieldIdx::from_usize(i)).discard_err() { - Some(field_op) => field_op, - // FIXME: render tuple fields independently so one - // projection failure does not throw away the whole - // source-shaped tuple. - None => return self.render_op(op), - }; - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); - } - - if fields.len() == 1 { - format!("({},)", fields[0]) - } else { - format!("({})", fields.join(", ")) - } - } - - ty::Array(_, _) | ty::Slice(_) => { - // `project_array_fields` uses the dynamic length for slices. That - // avoids the classic mistake of treating slice layout as a fixed - // zero-length array. - let mut iter = match self.ecx.project_array_fields(&op).discard_err() { - Some(iter) => iter, - // FIXME: when slice metadata is invalid, show that as a slice - // length problem instead of silently falling back to raw bytes. - None => return self.render_op(op), - }; - - let mut fields = Vec::new(); - // FIXME: add an output budget/truncation policy before rendering - // very large arrays or slices in full. - loop { - match iter.next(&self.ecx).discard_err() { - Some(Some((_idx, field_op))) => - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)), - Some(None) => break, - // FIXME: keep already-rendered elements and mark the - // failed index once partial render errors are supported. - None => return self.render_op(op), - } - } - - format!("[{}]", fields.join(", ")) - } - - // FIXME: consider source-shaped special cases for strings, closures, - // generators/coroutines, trait objects, and SIMD/vector-like types. - // Until then these stay on the raw renderer path. - _ => self.render_op(op), - } - } - - /// Render an evaluated operand using the same raw representation for - /// whole locals and projected MIR places. - fn render_op(&self, op: OpTy<'tcx>) -> String { - match op.as_mplace_or_imm() { - Either::Right(imm) => format!("{imm}"), - - Either::Left(mplace) => - match self.render_mplace_bytes(&mplace).report_err() { - Ok(bytes) => bytes, - Err(err) => format!("", err.to_string()), - }, - } - } - - /// Render the source-side path from composite debug info, such as `.field`. - fn render_source_projection( - fragment: Option<&VarDebugInfoFragment<'tcx>>, - ) -> Option> { - let VarDebugInfoFragment { ty, projection } = fragment?; - - // Walk the source-side projection from the original - // composite variable type. Each `Field` element stores the - // resulting field type, so resolve the field name from the - // current base type before advancing to `field_ty`. - let mut projection_ty = ty; - - Some( - projection - .iter() - .map(|elem| { - match elem { - ProjectionElem::Field(field_idx, field_ty) => { - let rendered = match projection_ty.kind() { - TyKind::Adt(adt_def, _args) if adt_def.is_struct() => { - let variant = adt_def.non_enum_variant(); - let field = &variant.fields[*field_idx]; - Symbol::intern(&format!(".{}", field.name)) - } - - TyKind::Tuple(_) => - Symbol::intern(&format!(".{}", field_idx.index())), - - _ => Symbol::intern("."), - }; - - projection_ty = field_ty; - - rendered - } - // `VarDebugInfoFragment::projection` is expected to be - // field-only. If that ever changes, keep the unexpected - // segment visible instead of silently rendering a - // misleading source path. - other => Symbol::intern(&format!(".")), - } - }) - .collect(), - ) - } - - /// Render the MIR storage-side path that backs a debug-info local. - fn render_storage_projection(projection: &[mir::PlaceElem<'tcx>]) -> Vec { - projection - .iter() - .map(|projection_elem| { - match projection_elem { - ProjectionElem::Field(field_idx, _) => StorageProj::Field(field_idx.index()), - ProjectionElem::Deref => StorageProj::Deref, - ProjectionElem::Downcast(Some(name), _) => StorageProj::Downcast(*name), - ProjectionElem::Downcast(None, variant_idx) => - StorageProj::Variant(variant_idx.index()), - other => StorageProj::Unsupported(format!("{other:?}")), - } - }) - .collect() - } - - /// Builds the baseline debugger row for one MIR local without scanning debug info. - fn make_mir_local_desc( - &self, - frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, - local: usize, - ) -> Option { - let local = mir::Local::from_usize(local); - let local_decl = frame.body().local_decls.get(local)?; - - // Create LocalDesc for MIR local before processing debug info. - // Debug-info enrichment is layered on by build_local_descs. - let mut local_desc = LocalDesc { - source_name: None, - source_projection: None, - local: Some(local), - storage_projection: Vec::new(), - ty: local_decl.ty.to_string(), - value: "".to_string(), - }; - - match &frame.locals[local].as_mplace_or_imm() { - None => { - local_desc.value = "".to_string(); - } - Some(Either::Right(Uninit)) => local_desc.value = "".to_string(), - - Some(Either::Left(_) | Either::Right(_)) => { - let op = self - .ecx - .local_to_op(local, None) - .expect("this error can only occur in CTFE on generic code"); - local_desc.value = self.render_source_shaped_op(op); - } - }; - - Some(local_desc) - } - - fn build_local_descs( - &self, - frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, - ) -> Vec { - let local_decls = &frame.body().local_decls; - - let mut local_descs: Vec = Vec::with_capacity(local_decls.len()); - - // Start with one baseline row for every MIR local, then layer debug info on top. - for (local_idx, _) in local_decls.iter_enumerated() { - local_descs.push(self.make_mir_local_desc(frame, local_idx.index()).unwrap()); - } - - // FIXME: Finish classifying `var_debug_info` by keeping the source path - // and MIR storage path separate: - // - // - source side: `var_debug_info.name` plus - // `var_debug_info.composite.projection` - // - storage side: `VarDebugInfoContents::Place(place).local` plus - // `place.projection` - // - // Already handled by the `place.as_local()` path below: - // - whole source variable -> whole MIR local: - // `composite = None`, `Place(_N)` with empty projection. - // - source fragment -> whole MIR local: - // `composite = Some(source_proj)`, `Place(_N)` with empty projection. - // - // Remaining cases to represent or explicitly defer: - // - whole source variable -> projected MIR storage: - // `composite = None`, `Place(_N.proj)`. - // - source fragment -> projected MIR storage: - // `composite = Some(source_proj)`, `Place(_N.storage_proj)`. - // - source variable/fragment -> constant: - // `Const(...)`, with no MIR local id. - // - optimized-out/debug-only/unsupported shapes: - // explicit deferred state, not silent discard. - // - // Final output should be produced by walking `Vec`, - // then append explicit deferred/debug-info-only rows where needed. - // Related: SROA can split a source local like `_slice: ExtraSlice` into - // field locals whose debug paths should be printed as `_slice._slice` - // and `_slice._extra`, not as two separate locals both named `_slice`. - - // Whole-place debug entries enrich the direct storage-local description. - // Projected places are evaluated from their original MIR Place and use - // the same raw renderer as ordinary locals. - for var_debug_info in &frame.body().var_debug_info { - if let VarDebugInfoContents::Place(place) = &var_debug_info.value { - if let Some(local_idx) = place.as_local() - && local_descs[local_idx.index()].source_name.is_none() - { - let local_idx = local_idx.index(); - local_descs[local_idx].source_projection = - Self::render_source_projection(var_debug_info.composite.as_deref()); - local_descs[local_idx].source_name = Some(var_debug_info.name); - } else if !place.projection.is_empty() { - let storage_projection = Self::render_storage_projection(place.projection); - let source_projection = - Self::render_source_projection(var_debug_info.composite.as_deref()); - let value = self - .ecx - .eval_place_to_op(*place, None) - .map(|op| self.render_source_shaped_op(op)) - .unwrap_or_else(|err| format!("", err.to_string())); - - local_descs.push(LocalDesc { - source_name: Some(var_debug_info.name), - source_projection, - local: Some(place.local), - storage_projection, - ty: place.ty(local_decls, self.ecx.tcx.tcx).ty.to_string(), - value, - }); - } - } - } - - local_descs - } -} - -enum DebuggerCommand { - StepI, - Step, - TerminateSession, - Continue, - Breakpoint(PathBuf, usize), - ListLocals, - Print(usize), - Follow(AllocId, usize), -} - -enum BreakpointSetResult { - Added(PathBuf, usize), - Duplicate, - // FIXME: add pending breakpoint support later if needed. -} - -enum CommandResult { - ExecutionStopped(StepResult), - BreakpointResult(BreakpointSetResult), - Locals(Vec), - SingleLocal(Option), - Memory(String), - // FIXME: distinguish terminating the debugger session from disconnecting a - // frontend and terminating the interpreted program once multiple frontends exist. - TerminateSession, -} - -struct Cli; - -impl Cli { - pub fn run_cli_loop<'tcx>(&self, session: &mut PrirodaContext<'tcx>) -> InterpResult<'tcx> { - loop { - print!("(priroda) "); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - let bytes_read = io::stdin().read_line(&mut input).unwrap(); - - if bytes_read == 0 { - println!("stdin closed, stopping"); - return interp_ok(()); - } - - if let Some(command) = self.parse_command(&input) { - let command_res = session.run_command(command)?; - if !Self::print_command_result(command_res, session)? { - return interp_ok(()); - }; - } else { - println!("no command"); - } - - io::stdout().flush().unwrap(); - } - } - - fn print_command_result<'tcx>( - command_res: CommandResult, - session: &PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, bool> { - match command_res { - CommandResult::ExecutionStopped(result) => { - if matches!(result, StepResult::Breakpoint) { - println!("Hit breakpoint"); - } - Self::print_location(session); - } - CommandResult::BreakpointResult(res) => - match res { - BreakpointSetResult::Added(path, line) => - println!("breakpoint added: {}:{}", path.display(), line), - - BreakpointSetResult::Duplicate => println!("Duplicate breakpoint"), - }, - CommandResult::Locals(locals_desc) => - if locals_desc.is_empty() { - println!("no locals"); - } else { - for local_desc in &locals_desc { - let source_projection = local_desc - .source_projection - .as_ref() - .map(|fields| { - fields.iter().map(|field| field.to_string()).collect::() - }) - .unwrap_or_default(); - - let name = local_desc - .source_name - .map_or_else(|| "".to_string(), |name| name.to_string()); - - let display_name = format!("{name}{source_projection}"); - - let local_id = local_desc.local.map_or_else( - || "".to_string(), - |local_idx| format!("_{}", local_idx.index()), - ); - - let storage_projection = local_desc - .storage_projection - .iter() - .map(StorageProj::render) - .collect::(); - - let display_local_id = format!("{local_id}{storage_projection}"); - println!( - "Name: {}, Id: {}, Ty: {}, Value: {}", - display_name, display_local_id, local_desc.ty, local_desc.value - ); - } - }, - CommandResult::SingleLocal(local_desc) => - match local_desc { - Some(local_desc) => { - println!( - "Id: _{}, Ty: {}, Value: {}", - local_desc.local.unwrap().index(), - local_desc.ty, - local_desc.value - ); - } - None => println!("no local for this id"), - }, - CommandResult::Memory(memory) => println!("{memory}"), - CommandResult::TerminateSession => { - println!("quitting"); - return interp_ok(false); - } - } - interp_ok(true) - } - - fn parse_command(&self, input: &str) -> Option { - // TODO: look at the Spanned crate for how to easily produce errors in - // rustc's style while manually parsing text input. - // FIXME: we need to distinguish malformed input from the unknown commands by returning useful - // command error that describes if it malformed or non exist command - let input = input.trim(); - let mut parts = input.splitn(2, char::is_whitespace); - let command = parts.next().unwrap_or(""); - let args = parts.next().unwrap_or("").trim(); - - match command { - // FIXME: empty line should repats last command user typed not exeute specific command. - "" | "si" | "stepi" => Some(DebuggerCommand::StepI), - "s" | "step" => Some(DebuggerCommand::Step), - "q" | "quit" => Some(DebuggerCommand::TerminateSession), - "c" | "continue" => Some(DebuggerCommand::Continue), - "b" | "break" => self.parse_breakpoint(args), - "l" | "locals" => Some(DebuggerCommand::ListLocals), - "p" | "print" => self.parse_print_local(args), - "f" | "follow" => self.parse_follow(args), - _ => None, - } - } - - fn print_location(session: &PrirodaContext) { - match &session.current_location { - Some(location) => - if let Some(path) = session.local_path(location) { - println!("{}:{}", path.display(), location.line); - } else { - let source_map = session.ecx.tcx.sess.source_map(); - println!("{}", source_map.span_to_diagnostic_string(location.span)); - }, - None => println!("no-location"), - } - io::stdout().flush().unwrap(); - } - - fn parse_breakpoint(&self, input: &str) -> Option { - // FIXME: return a typed CommandError so malformed breakpoint input is - // distinguishable from an unknown command. Semantic validation belongs - // in PrirodaContext::set_breakpoint so non-CLI frontends cannot bypass it. - let (path, line) = input.rsplit_once(':')?; - let line = line.parse().ok()?; - - Some(DebuggerCommand::Breakpoint(PathBuf::from(path), line)) - } - - fn parse_print_local(&self, input: &str) -> Option { - let local = input.parse().ok()?; - Some(DebuggerCommand::Print(local)) - } - - fn parse_follow(&self, input: &str) -> Option { - let mut parts = input.split_whitespace(); - let alloc_id = parts.next()?; - let offset = parts.next()?; - if parts.next().is_some() { - return None; - } - - let alloc_id = alloc_id.strip_prefix("alloc").unwrap_or(alloc_id).parse().ok()?; - let alloc_id = AllocId(NonZeroU64::new(alloc_id)?); - let offset = offset.parse().ok()?; - Some(DebuggerCommand::Follow(alloc_id, offset)) - } -} - -struct Dap; - -impl Dap { - pub fn run_dap_loop<'tcx>(&self, _session: &mut PrirodaContext<'tcx>) -> InterpResult<'tcx> { - // FIXME: implement DAP framing and request dispatch on top of PrirodaContext. - interp_ok(()) - } -} From a6a7a9d757fd9a5529e7df4b89fdbf2627878261 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Thu, 30 Jul 2026 00:24:01 +0300 Subject: [PATCH 10/42] [Priroda] Add minimal DAP initialize handshake --- src/tools/miri/priroda/Cargo.lock | 12 +++ src/tools/miri/priroda/Cargo.toml | 1 + src/tools/miri/priroda/src/frontend/dap.rs | 95 ++++++++++++++++++- src/tools/miri/priroda/tests/cli.rs | 3 + .../priroda/tests/ui/dap_initialize.stdout | 3 + .../dap_rejects_non_initialize_first.stderr | 1 + .../dap_rejects_non_initialize_first.stdout | 3 + 7 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr diff --git a/src/tools/miri/priroda/Cargo.lock b/src/tools/miri/priroda/Cargo.lock index 7d46f75d2fab7..48ba54ef8eebf 100644 --- a/src/tools/miri/priroda/Cargo.lock +++ b/src/tools/miri/priroda/Cargo.lock @@ -351,6 +351,17 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "emmy_dap_types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2310ff06ab812a0332ffa037bbda9d994b3721a7f8a308ff38c28bdb20c37f56" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -891,6 +902,7 @@ dependencies = [ name = "priroda" version = "0.1.0" dependencies = [ + "emmy_dap_types", "miri", "regex", "ui_test", diff --git a/src/tools/miri/priroda/Cargo.toml b/src/tools/miri/priroda/Cargo.toml index 88e65653449c8..ff299bae2acbf 100644 --- a/src/tools/miri/priroda/Cargo.toml +++ b/src/tools/miri/priroda/Cargo.toml @@ -18,6 +18,7 @@ name = "cli" harness = false [dependencies] +emmy_dap_types = "0.2.0" miri = { path = ".." } [package.metadata.rust-analyzer] diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 75536910aacac..31a1447aa3642 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,7 +1,19 @@ +use std::io::{self, BufReader, BufWriter}; + +use emmy_dap_types::prelude::types::Capabilities; +use emmy_dap_types::prelude::{Command, Request, ResponseBody, Server}; use miri::{InterpResult, interp_ok}; use crate::debugger::PrirodaContext; +const MAX_REQUEST_COUNT: usize = 128; +type ServerResult = Result; + +enum DispatchOutcome { + Continue, + Exit, +} + /// Debug Adapter Protocol frontend. pub(crate) struct Dap; @@ -11,7 +23,88 @@ impl Dap { &self, _session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx> { - // FIXME: implement DAP framing and request dispatch on top of PrirodaContext. + // FIXME: make this unbounded once Priroda has a full session lifecycle. + if let Err(err) = DapSession::stdio().run_requests() { + eprintln!("priroda dap error: {err}"); + } + interp_ok(()) } } + +type DapServer = Server, io::StdoutLock<'static>>; + +/// Owns the DAP stdio transport and dispatches requests into Priroda handlers. +struct DapSession { + server: DapServer, +} + +impl DapSession { + fn stdio() -> Self { + Self { + server: Server::new( + BufReader::new(io::stdin().lock()), + BufWriter::new(io::stdout().lock()), + ), + } + } + + fn run_requests(&mut self) -> ServerResult { + for _ in 0..MAX_REQUEST_COUNT { + let Some(request) = self.server.poll_request()? else { + return Ok(()); + }; + + match self.dispatch_request(request)? { + DispatchOutcome::Continue => {} + DispatchOutcome::Exit => return Ok(()), + } + } + + Ok(()) + } + + fn dispatch_request(&mut self, request: Request) -> ServerResult { + match &request.command { + Command::Initialize(_) => + self.handle_initialize(request).map(|()| DispatchOutcome::Continue), + _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + } + } + + /// FIXME: grow capabilities as Priroda adds DAP features. + fn handle_initialize(&mut self, request: Request) -> ServerResult { + // Advertise configurationDone support ahead of its handler so VS Code + // completes the full handshake; the handler arrives in a later commit. + let response = request.success(ResponseBody::Initialize(Capabilities { + supports_configuration_done_request: Some(true), + ..Capabilities::default() + })); + self.server.respond(response) + } + + fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { + eprintln!( + "priroda dap: unsupported request during DAP demo milestone: {}", + Self::display_command(&request.command) + ); + let response = request.error("unsupported request in Priroda DAP demo mode"); + self.server.respond(response) + } + + fn display_command(command: &Command) -> &'static str { + match command { + Command::Initialize(_) => "initialize", + Command::Launch(_) => "launch", + Command::ConfigurationDone => "configurationDone", + Command::Threads => "threads", + Command::StackTrace(_) => "stackTrace", + Command::Scopes(_) => "scopes", + Command::Variables(_) => "variables", + Command::Next(_) => "next", + Command::StepIn(_) => "stepIn", + Command::Disconnect(_) => "disconnect", + _ => "unsupported", + } + } +} diff --git a/src/tools/miri/priroda/tests/cli.rs b/src/tools/miri/priroda/tests/cli.rs index 3b596fbf91f26..ff2ce7716348a 100644 --- a/src/tools/miri/priroda/tests/cli.rs +++ b/src/tools/miri/priroda/tests/cli.rs @@ -33,11 +33,14 @@ fn main() -> Result<(), Box> { let miri_dir_regex = Regex::new(®ex::escape(&miri_dir.display().to_string())).unwrap(); let rustc_sysroot_regex = Regex::new(®ex::escape(&rustc_sysroot)).unwrap(); let pointer_regex = Regex::new(r"0x[0-9a-f]+\[alloc[0-9]+\]<[0-9]+>").unwrap(); + let crlf_regex = Regex::new(r"\r\n").unwrap(); config.comment_defaults.base().normalize_stdout.extend([ (manifest_dir_regex.into(), b"{MANIFEST_DIR}".to_vec()), (miri_dir_regex.into(), b"{MIRI_DIR}".to_vec()), (rustc_sysroot_regex.into(), b"{RUSTC_SYSROOT}".to_vec()), (pointer_regex.into(), b"{ALLOC_PTR}".to_vec()), + // DAP frames use CRLF headers; keep checked-in stdout fixtures readable. + (crlf_regex.into(), b"\n".to_vec()), ]); // Priroda CLI tests do not currently require annotation comments in the test files diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout index e69de29bb2d1d..4773b876a1df8 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout @@ -0,0 +1,3 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr new file mode 100644 index 0000000000000..2641eb804868c --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr @@ -0,0 +1 @@ +priroda dap: unsupported request during DAP demo milestone: next diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout index e69de29bb2d1d..18ed56545ac9d 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout @@ -0,0 +1,3 @@ +Content-Length: 146 + +{"seq":1,"type":"response","request_seq":2,"success":false,"message":"unsupported request in Priroda DAP demo mode","command":"next","error":null} \ No newline at end of file From 0158e09b0938e2992cf73b3ac94de922cf93ab39 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 02:26:36 +0300 Subject: [PATCH 11/42] [Priroda] Add DAP initialized and launch handshake --- src/tools/miri/priroda/src/frontend/dap.rs | 12 ++++++++++-- .../miri/priroda/tests/ui/dap_initialize.stdout | 4 +++- .../miri/priroda/tests/ui/dap_initialize_launch.rs | 3 +++ .../priroda/tests/ui/dap_initialize_launch.stdin | 5 +++++ .../priroda/tests/ui/dap_initialize_launch.stdout | 7 +++++++ 5 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 31a1447aa3642..9d6d77f2a8d44 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,7 +1,7 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::types::Capabilities; -use emmy_dap_types::prelude::{Command, Request, ResponseBody, Server}; +use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, interp_ok}; use crate::debugger::PrirodaContext; @@ -68,10 +68,17 @@ impl DapSession { match &request.command { Command::Initialize(_) => self.handle_initialize(request).map(|()| DispatchOutcome::Continue), + Command::Launch(_) => self.handle_launch(request).map(|()| DispatchOutcome::Continue), _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), } } + /// FIXME: connect launch arguments to Priroda's session model. + fn handle_launch(&mut self, request: Request) -> ServerResult { + let response = request.success(ResponseBody::Launch); + self.server.respond(response) + } + /// FIXME: grow capabilities as Priroda adds DAP features. fn handle_initialize(&mut self, request: Request) -> ServerResult { // Advertise configurationDone support ahead of its handler so VS Code @@ -80,7 +87,8 @@ impl DapSession { supports_configuration_done_request: Some(true), ..Capabilities::default() })); - self.server.respond(response) + self.server.respond(response)?; + self.server.send_event(Event::Initialized) } fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout index 4773b876a1df8..595a84f405b40 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout @@ -1,3 +1,5 @@ Content-Length: 143 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null} \ No newline at end of file +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.rs b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdin b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdin new file mode 100644 index 0000000000000..ae4ee94ca0e98 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdin @@ -0,0 +1,5 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout new file mode 100644 index 0000000000000..1bf97320e659e --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout @@ -0,0 +1,7 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null} \ No newline at end of file From 5e8f7b9de1f322f27cfe9af1da8519ebdd2f1e5b Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 02:51:49 +0300 Subject: [PATCH 12/42] [Priroda] Handle DAP configurationDone startup request --- src/tools/miri/priroda/src/frontend/dap.rs | 7 +++++++ .../tests/ui/dap_initialize_launch_configuration_done.rs | 3 +++ .../ui/dap_initialize_launch_configuration_done.stdin | 7 +++++++ .../ui/dap_initialize_launch_configuration_done.stdout | 9 +++++++++ 4 files changed, 26 insertions(+) create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 9d6d77f2a8d44..c2a63ed4e9be6 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -69,6 +69,8 @@ impl DapSession { Command::Initialize(_) => self.handle_initialize(request).map(|()| DispatchOutcome::Continue), Command::Launch(_) => self.handle_launch(request).map(|()| DispatchOutcome::Continue), + Command::ConfigurationDone => + self.handle_configuration_done(request).map(|()| DispatchOutcome::Continue), _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), } } @@ -79,6 +81,11 @@ impl DapSession { self.server.respond(response) } + fn handle_configuration_done(&mut self, request: Request) -> ServerResult { + let response = request.success(ResponseBody::ConfigurationDone); + self.server.respond(response) + } + /// FIXME: grow capabilities as Priroda adds DAP features. fn handle_initialize(&mut self, request: Request) -> ServerResult { // Advertise configurationDone support ahead of its handler so VS Code diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.rs b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin new file mode 100644 index 0000000000000..106ce5dac35e0 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin @@ -0,0 +1,7 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout new file mode 100644 index 0000000000000..d4b48cf24093d --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout @@ -0,0 +1,9 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null} \ No newline at end of file From 8cb2e7cfb18a4530dbbe84a60239939266a6a7ca Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 02:58:49 +0300 Subject: [PATCH 13/42] [Priroda] Handle DAP threads request --- src/tools/miri/priroda/src/frontend/dap.rs | 14 +++++++++++++- src/tools/miri/priroda/tests/ui/dap_threads.rs | 3 +++ src/tools/miri/priroda/tests/ui/dap_threads.stdin | 9 +++++++++ src/tools/miri/priroda/tests/ui/dap_threads.stdout | 11 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_threads.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_threads.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_threads.stdout diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index c2a63ed4e9be6..4425662116fce 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,11 +1,13 @@ use std::io::{self, BufReader, BufWriter}; -use emmy_dap_types::prelude::types::Capabilities; +use emmy_dap_types::prelude::responses::ThreadsResponse; +use emmy_dap_types::prelude::types::{Capabilities, Thread}; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, interp_ok}; use crate::debugger::PrirodaContext; +const THREAD_ID: i64 = 1; const MAX_REQUEST_COUNT: usize = 128; type ServerResult = Result; @@ -71,6 +73,7 @@ impl DapSession { Command::Launch(_) => self.handle_launch(request).map(|()| DispatchOutcome::Continue), Command::ConfigurationDone => self.handle_configuration_done(request).map(|()| DispatchOutcome::Continue), + Command::Threads => self.handle_threads(request).map(|()| DispatchOutcome::Continue), _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), } } @@ -86,6 +89,15 @@ impl DapSession { self.server.respond(response) } + /// FIXME: replace this with Miri thread state once Priroda exposes a + /// frontend-facing thread model. + fn handle_threads(&mut self, request: Request) -> ServerResult { + let response = request.success(ResponseBody::Threads(ThreadsResponse { + threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], + })); + self.server.respond(response) + } + /// FIXME: grow capabilities as Priroda adds DAP features. fn handle_initialize(&mut self, request: Request) -> ServerResult { // Advertise configurationDone support ahead of its handler so VS Code diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.rs b/src/tools/miri/priroda/tests/ui/dap_threads.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_threads.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.stdin b/src/tools/miri/priroda/tests/ui/dap_threads.stdin new file mode 100644 index 0000000000000..a17c6406c9c73 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdin @@ -0,0 +1,9 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 46 + +{"seq":4,"type":"request","command":"threads"} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.stdout b/src/tools/miri/priroda/tests/ui/dap_threads.stdout new file mode 100644 index 0000000000000..14407c49aca87 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdout @@ -0,0 +1,11 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 135 + +{"seq":5,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null} \ No newline at end of file From 5a29686e75616d1db4d5444214aa7c89ae9d5769 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 03:03:24 +0300 Subject: [PATCH 14/42] [Priroda] Handle DAP stackTrace request --- src/tools/miri/priroda/src/frontend/dap.rs | 13 ++++++++++++- src/tools/miri/priroda/tests/ui/dap_stack_trace.rs | 3 +++ .../miri/priroda/tests/ui/dap_stack_trace.stdin | 11 +++++++++++ .../miri/priroda/tests/ui/dap_stack_trace.stdout | 13 +++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_stack_trace.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_stack_trace.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 4425662116fce..785601e09df91 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,6 +1,6 @@ use std::io::{self, BufReader, BufWriter}; -use emmy_dap_types::prelude::responses::ThreadsResponse; +use emmy_dap_types::prelude::responses::{StackTraceResponse, ThreadsResponse}; use emmy_dap_types::prelude::types::{Capabilities, Thread}; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, interp_ok}; @@ -74,6 +74,8 @@ impl DapSession { Command::ConfigurationDone => self.handle_configuration_done(request).map(|()| DispatchOutcome::Continue), Command::Threads => self.handle_threads(request).map(|()| DispatchOutcome::Continue), + Command::StackTrace(_) => + self.handle_stack_trace(request).map(|()| DispatchOutcome::Continue), _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), } } @@ -98,6 +100,15 @@ impl DapSession { self.server.respond(response) } + /// FIXME: report real frames once Priroda exposes a frontend-facing stack model. + fn handle_stack_trace(&mut self, request: Request) -> ServerResult { + let response = request.success(ResponseBody::StackTrace(StackTraceResponse { + stack_frames: Vec::new(), + total_frames: Some(0), + })); + self.server.respond(response) + } + /// FIXME: grow capabilities as Priroda adds DAP features. fn handle_initialize(&mut self, request: Request) -> ServerResult { // Advertise configurationDone support ahead of its handler so VS Code diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.rs b/src/tools/miri/priroda/tests/ui/dap_stack_trace.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdin b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdin new file mode 100644 index 0000000000000..1056beef5712e --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdin @@ -0,0 +1,11 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 46 + +{"seq":4,"type":"request","command":"threads"}Content-Length: 76 + +{"seq":5,"type":"request","command":"stackTrace","arguments":{"threadId":1}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout new file mode 100644 index 0000000000000..4229c6cd473aa --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -0,0 +1,13 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 135 + +{"seq":5,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 136 + +{"seq":6,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[],"totalFrames":0},"error":null} \ No newline at end of file From 2d7ddb651cae31dbbc5cefbb269650a5fea21b4e Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 18:26:14 +0300 Subject: [PATCH 15/42] [Priroda] Add core debugger stop-at-first-user-location Add FirstUserSourceLocation ResumeMode variant that stops when the interpreter reaches a user-relevant frame with a source location. This gives the DAP frontend an entry-stop primitive that skips Miri-internal and std frames. --- src/tools/miri/priroda/src/debugger.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index aa4cb45b85aba..ee570cffa8f9e 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -100,6 +100,8 @@ enum ResumeMode { /// /// Take `Option` because some cases current state has no mapped to source code location SourceLine(Option<(PathBuf, usize)>), + /// Stop at the first mapped source location from a user-relevant frame. + FirstUserSourceLocation, /// Continue until reaching a breakpoint. Continue, } @@ -152,6 +154,10 @@ impl<'tcx> PrirodaContext<'tcx> { self.resume(ResumeMode::SourceLine(self.current_source_position())) } + pub(super) fn stop_at_first_user_location(&mut self) -> InterpResult<'tcx, StepResult> { + self.resume(ResumeMode::FirstUserSourceLocation) + } + /// Continue execution until reaching a breakpoint or propagating termination. fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::Continue) @@ -210,11 +216,26 @@ impl<'tcx> PrirodaContext<'tcx> { } } - ResumeMode::MirInstruction | ResumeMode::Continue => {} + ResumeMode::FirstUserSourceLocation + if self.current_location.is_some() && self.has_user_relevant_frame() => + { + return interp_ok(StepResult::Step); + } + + ResumeMode::MirInstruction + | ResumeMode::FirstUserSourceLocation + | ResumeMode::Continue => {} } } } + fn has_user_relevant_frame(&self) -> bool { + // Walk the whole stack, not just the top frame: during interpreter + // startup the user's `main` can sit under Miri-internal frames that + // have no source span, so checking only `last()` would miss it. + self.ecx.active_thread_stack().iter().any(|frame| frame.extra.user_relevance == u8::MAX) + } + /// Advance Miri by one interpreter-loop transition. fn advance(&mut self) -> InterpResult<'tcx> { // FIXME: use a Miri-owned scheduler-aware debugger step API before From dc713762c74f73e17caf2f42587b34aaddd98be6 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 18:26:14 +0300 Subject: [PATCH 16/42] [Priroda] Wire DAP to interpreter lifecycle and stopped event --- src/tools/miri/priroda/src/frontend/dap.rs | 91 ++++++++++++++----- ...nitialize_launch_configuration_done.stdout | 4 +- .../priroda/tests/ui/dap_stack_trace.stdout | 8 +- .../miri/priroda/tests/ui/dap_threads.stdout | 6 +- 4 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 785601e09df91..600cdf764cdc3 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,7 +1,8 @@ use std::io::{self, BufReader, BufWriter}; +use emmy_dap_types::prelude::events::StoppedEventBody; use emmy_dap_types::prelude::responses::{StackTraceResponse, ThreadsResponse}; -use emmy_dap_types::prelude::types::{Capabilities, Thread}; +use emmy_dap_types::prelude::types::{Capabilities, StoppedEventReason, Thread}; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, interp_ok}; @@ -23,10 +24,10 @@ impl Dap { /// Serve DAP requests on stdin/stdout. pub(crate) fn run_dap_loop<'tcx>( &self, - _session: &mut PrirodaContext<'tcx>, + session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx> { // FIXME: make this unbounded once Priroda has a full session lifecycle. - if let Err(err) = DapSession::stdio().run_requests() { + if let Err(err) = DapSession::stdio().run_requests(session)? { eprintln!("priroda dap error: {err}"); } @@ -39,6 +40,7 @@ type DapServer = Server, io::StdoutLock<'static>>; /// Owns the DAP stdio transport and dispatches requests into Priroda handlers. struct DapSession { server: DapServer, + initialized: bool, } impl DapSession { @@ -48,35 +50,59 @@ impl DapSession { BufReader::new(io::stdin().lock()), BufWriter::new(io::stdout().lock()), ), + initialized: false, } } - fn run_requests(&mut self) -> ServerResult { + fn run_requests<'tcx>( + &mut self, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, ServerResult> { for _ in 0..MAX_REQUEST_COUNT { - let Some(request) = self.server.poll_request()? else { - return Ok(()); + let request = match self.server.poll_request() { + Ok(Some(request)) => request, + Ok(None) => return interp_ok(Ok(())), + Err(err) => return interp_ok(Err(err)), }; - match self.dispatch_request(request)? { - DispatchOutcome::Continue => {} - DispatchOutcome::Exit => return Ok(()), + match self.dispatch_request(request, session)? { + Ok(DispatchOutcome::Continue) => {} + Ok(DispatchOutcome::Exit) => return interp_ok(Ok(())), + Err(err) => return interp_ok(Err(err)), } } - Ok(()) + interp_ok(Ok(())) } - fn dispatch_request(&mut self, request: Request) -> ServerResult { + fn dispatch_request<'tcx>( + &mut self, + request: Request, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, ServerResult> { + // Reject non-initialize requests before the handshake completes so the + // client gets a framed error. + if !self.initialized && !matches!(&request.command, Command::Initialize(_)) { + return interp_ok( + self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + ); + } + match &request.command { Command::Initialize(_) => - self.handle_initialize(request).map(|()| DispatchOutcome::Continue), - Command::Launch(_) => self.handle_launch(request).map(|()| DispatchOutcome::Continue), - Command::ConfigurationDone => - self.handle_configuration_done(request).map(|()| DispatchOutcome::Continue), - Command::Threads => self.handle_threads(request).map(|()| DispatchOutcome::Continue), + interp_ok(self.handle_initialize(request).map(|()| DispatchOutcome::Continue)), + Command::Launch(_) => + interp_ok(self.handle_launch(request).map(|()| DispatchOutcome::Continue)), + Command::ConfigurationDone => { + let res = self.handle_configuration_done(request, session)?; + interp_ok(res.map(|()| DispatchOutcome::Continue)) + } + Command::Threads => + interp_ok(self.handle_threads(request).map(|()| DispatchOutcome::Continue)), Command::StackTrace(_) => - self.handle_stack_trace(request).map(|()| DispatchOutcome::Continue), - _ => self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + interp_ok(self.handle_stack_trace(request).map(|()| DispatchOutcome::Continue)), + _ => + interp_ok(self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit)), } } @@ -86,9 +112,18 @@ impl DapSession { self.server.respond(response) } - fn handle_configuration_done(&mut self, request: Request) -> ServerResult { + fn handle_configuration_done<'tcx>( + &mut self, + request: Request, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, ServerResult> { + session.stop_at_first_user_location()?; let response = request.success(ResponseBody::ConfigurationDone); - self.server.respond(response) + interp_ok( + self.server + .respond(response) + .and_then(|()| self.send_stopped_event(StoppedEventReason::Entry)), + ) } /// FIXME: replace this with Miri thread state once Priroda exposes a @@ -118,7 +153,9 @@ impl DapSession { ..Capabilities::default() })); self.server.respond(response)?; - self.server.send_event(Event::Initialized) + self.server.send_event(Event::Initialized)?; + self.initialized = true; + Ok(()) } fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { @@ -130,6 +167,18 @@ impl DapSession { self.server.respond(response) } + fn send_stopped_event(&mut self, reason: StoppedEventReason) -> ServerResult { + self.server.send_event(Event::Stopped(StoppedEventBody { + reason, + description: None, + thread_id: Some(THREAD_ID), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + } + fn display_command(command: &Command) -> &'static str { match command { Command::Initialize(_) => "initialize", diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout index d4b48cf24093d..af8fbfd94f4fa 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout @@ -6,4 +6,6 @@ Content-Length: 143 {"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null} \ No newline at end of file +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 + +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout index 4229c6cd473aa..88cf8f01933db 100644 --- a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -6,8 +6,10 @@ Content-Length: 143 {"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 135 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 -{"seq":5,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 136 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 -{"seq":6,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[],"totalFrames":0},"error":null} \ No newline at end of file +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 136 + +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[],"totalFrames":0},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.stdout b/src/tools/miri/priroda/tests/ui/dap_threads.stdout index 14407c49aca87..953d114e22ec2 100644 --- a/src/tools/miri/priroda/tests/ui/dap_threads.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdout @@ -6,6 +6,8 @@ Content-Length: 143 {"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 135 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 -{"seq":5,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null} \ No newline at end of file +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 + +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null} \ No newline at end of file From 84ec755e41f2cb2d27fe6912799d992cdd21f51e Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 18:27:15 +0300 Subject: [PATCH 17/42] [Priroda] Report current DAP stack frame --- src/tools/miri/priroda/src/debugger.rs | 8 ++- src/tools/miri/priroda/src/frontend/dap.rs | 60 ++++++++++++++++--- .../priroda/tests/ui/dap_stack_trace.stdout | 4 +- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index ee570cffa8f9e..cddd55b77ba68 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -17,6 +17,7 @@ pub(super) struct SourceLocation { // storing `span` to use it lazily to compute path. pub(super) span: Span, pub(super) line: usize, + pub(super) column: usize, } impl SourceLocation { @@ -158,6 +159,11 @@ impl<'tcx> PrirodaContext<'tcx> { self.resume(ResumeMode::FirstUserSourceLocation) } + pub(super) fn current_frame_name(&self) -> Option { + let frame = self.ecx.active_thread_stack().last()?; + Some(frame.instance().to_string()) + } + /// Continue execution until reaching a breakpoint or propagating termination. fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::Continue) @@ -304,7 +310,7 @@ impl<'tcx> PrirodaContext<'tcx> { let source_map = self.ecx.tcx.sess.source_map(); let loc = source_map.lookup_char_pos(span.lo()); - Some(SourceLocation { span, line: loc.line }) + Some(SourceLocation { span, line: loc.line, column: loc.col_display + 1 }) } pub(super) fn run_command( diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 600cdf764cdc3..3b6cdb01fc981 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -2,13 +2,16 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::events::StoppedEventBody; use emmy_dap_types::prelude::responses::{StackTraceResponse, ThreadsResponse}; -use emmy_dap_types::prelude::types::{Capabilities, StoppedEventReason, Thread}; +use emmy_dap_types::prelude::types::{ + Capabilities, Source, StackFrame, StoppedEventReason, Thread, +}; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; -use miri::{InterpResult, interp_ok}; +use miri::{InterpResult, bug, interp_ok}; use crate::debugger::PrirodaContext; const THREAD_ID: i64 = 1; +const STACK_FRAME_ID: i64 = 1; const MAX_REQUEST_COUNT: usize = 128; type ServerResult = Result; @@ -100,7 +103,9 @@ impl DapSession { Command::Threads => interp_ok(self.handle_threads(request).map(|()| DispatchOutcome::Continue)), Command::StackTrace(_) => - interp_ok(self.handle_stack_trace(request).map(|()| DispatchOutcome::Continue)), + interp_ok( + self.handle_stack_trace(request, session).map(|()| DispatchOutcome::Continue), + ), _ => interp_ok(self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit)), } @@ -135,11 +140,52 @@ impl DapSession { self.server.respond(response) } - /// FIXME: report real frames once Priroda exposes a frontend-facing stack model. - fn handle_stack_trace(&mut self, request: Request) -> ServerResult { + /// FIXME: report all frames once Priroda exposes a frontend-facing stack model. + fn handle_stack_trace<'tcx>( + &mut self, + request: Request, + session: &PrirodaContext<'tcx>, + ) -> ServerResult { + let stack_frames = match &session.current_location { + Some(location) => { + let path = session.local_path(location); + vec![StackFrame { + id: STACK_FRAME_ID, + name: session.current_frame_name().unwrap_or_else(|| "".to_string()), + source: path.as_ref().map(|path| { + Source { + name: path.file_name().map(|name| name.to_string_lossy().into_owned()), + path: Some(path.display().to_string()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + checksums: None, + } + }), + line: location + .line + .try_into() + .unwrap_or_else(|_| bug!("source line exceeds i64")), + column: location + .column + .try_into() + .unwrap_or_else(|_| bug!("source column exceeds i64")), + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }] + } + None => Vec::new(), + }; + let total_frames: i64 = + stack_frames.len().try_into().unwrap_or_else(|_| bug!("frame count exceeds i64")); let response = request.success(ResponseBody::StackTrace(StackTraceResponse { - stack_frames: Vec::new(), - total_frames: Some(0), + stack_frames, + total_frames: Some(total_frames), })); self.server.respond(response) } diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout index 88cf8f01933db..318eefe286bac 100644 --- a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -10,6 +10,6 @@ Content-Length: 143 {"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 136 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 295 -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[],"totalFrames":0},"error":null} \ No newline at end of file +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_stack_trace.rs","path":"{MANIFEST_DIR}/tests/ui/dap_stack_trace.rs"},"line":3,"column":11}],"totalFrames":1},"error":null} \ No newline at end of file From abef4585be35f2cade9e7fc3daf208dca46fa2cc Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sat, 1 Aug 2026 18:27:50 +0300 Subject: [PATCH 18/42] [Priroda] Add DAP locals scope and variables --- src/tools/miri/priroda/src/debugger.rs | 2 +- src/tools/miri/priroda/src/frontend/dap.rs | 86 ++++++++++++++++++- .../priroda/tests/ui/dap_scopes_variables.rs | 7 ++ .../tests/ui/dap_scopes_variables.stdin | 13 +++ .../tests/ui/dap_scopes_variables.stdout | 17 ++++ 5 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index cddd55b77ba68..b40ae12cdc396 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -356,7 +356,7 @@ impl<'tcx> PrirodaContext<'tcx> { /// /// Starts from all MIR locals, then enriches them with source names from /// `var_debug_info` when a debug entry maps directly to a whole local. - fn list_locals(&self) -> Vec { + pub(super) fn list_locals(&self) -> Vec { let Some(frame) = self.ecx.active_thread_stack().last() else { return Vec::new(); }; diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 3b6cdb01fc981..01c3fbd2d5019 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,17 +1,21 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::events::StoppedEventBody; -use emmy_dap_types::prelude::responses::{StackTraceResponse, ThreadsResponse}; +use emmy_dap_types::prelude::responses::{ + ScopesResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, +}; use emmy_dap_types::prelude::types::{ - Capabilities, Source, StackFrame, StoppedEventReason, Thread, + Capabilities, Scope, ScopePresentationhint, Source, StackFrame, StoppedEventReason, Thread, + Variable, }; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, bug, interp_ok}; -use crate::debugger::PrirodaContext; +use crate::debugger::{LocalDesc, PrirodaContext}; const THREAD_ID: i64 = 1; const STACK_FRAME_ID: i64 = 1; +const LOCALS_VARIABLES_REFERENCE: i64 = 1; const MAX_REQUEST_COUNT: usize = 128; type ServerResult = Result; @@ -106,6 +110,12 @@ impl DapSession { interp_ok( self.handle_stack_trace(request, session).map(|()| DispatchOutcome::Continue), ), + Command::Scopes(_) => + interp_ok(self.handle_scopes(request, session).map(|()| DispatchOutcome::Continue)), + Command::Variables(_) => + interp_ok( + self.handle_variables(request, session).map(|()| DispatchOutcome::Continue), + ), _ => interp_ok(self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit)), } @@ -117,6 +127,45 @@ impl DapSession { self.server.respond(response) } + fn handle_scopes<'tcx>( + &mut self, + request: Request, + _session: &PrirodaContext<'tcx>, + ) -> ServerResult { + let response = request.success(ResponseBody::Scopes(ScopesResponse { + scopes: vec![Scope { + name: "Locals".to_string(), + presentation_hint: Some(ScopePresentationhint::Locals), + variables_reference: LOCALS_VARIABLES_REFERENCE, + named_variables: None, + indexed_variables: Some(0), + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }], + })); + self.server.respond(response) + } + + fn handle_variables<'tcx>( + &mut self, + request: Request, + session: &PrirodaContext<'tcx>, + ) -> ServerResult { + let variables = match &request.command { + Command::Variables(args) if args.variables_reference == LOCALS_VARIABLES_REFERENCE => + session.list_locals().into_iter().map(Self::local_to_variable).collect(), + Command::Variables(_) => Vec::new(), + _ => unreachable!(), + }; + + let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); + self.server.respond(response) + } + fn handle_configuration_done<'tcx>( &mut self, request: Request, @@ -240,4 +289,35 @@ impl DapSession { _ => "unsupported", } } + + fn local_to_variable(local: LocalDesc) -> Variable { + Variable { + name: Self::local_name(&local), + value: local.value, + type_field: Some(local.ty), + presentation_hint: None, + evaluate_name: None, + // FIXME: add child handles once Priroda can identify places across requests. + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + } + } + + fn local_name(local: &LocalDesc) -> String { + let source_projection = local.source_projection_str(); + + // Prefer source names when debug info gives us one. If a local only has + // MIR storage identity, keep that visible so the DAP Variables view + // still has a stable row for every backing local. + if let Some(source_name) = local.source_name { + return format!("{source_name}{source_projection}"); + } + + let local_id = local + .local + .map_or_else(|| "".to_string(), |local_idx| format!("_{}", local_idx.index())); + format!("{local_id}{}", local.storage_projection_str()) + } } diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.rs b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.rs new file mode 100644 index 0000000000000..081c3ce1d97c6 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.rs @@ -0,0 +1,7 @@ +//@ compile-flags: --dap + +fn main() { + let x = 1_i32; + let y = true; + let _ = (x, y); +} diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdin b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdin new file mode 100644 index 0000000000000..d1dd783eb96fa --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdin @@ -0,0 +1,13 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 76 + +{"seq":4,"type":"request","command":"stackTrace","arguments":{"threadId":1}}Content-Length: 71 + +{"seq":5,"type":"request","command":"scopes","arguments":{"frameId":1}}Content-Length: 85 + +{"seq":6,"type":"request","command":"variables","arguments":{"variablesReference":1}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout new file mode 100644 index 0000000000000..d7a47ffb2d178 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout @@ -0,0 +1,17 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 + +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 304 + +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs"},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 + +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 + +{"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null} \ No newline at end of file From d233df9d66cda4273dc32e95f473f58e807c5c93 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:33:55 +0300 Subject: [PATCH 19/42] [Priroda] Add bounded DAP source-line stepping demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire `next` and `stepIn` DAP requests to Priroda's existing source-line step. Both commands use the same `handle_step` handler for now; true step-over vs step-in semantics are deferred. Add `stopped_reason` to map `StepResult` variants to DAP `StoppedEventReason` so the editor can distinguish a manual step from a breakpoint hit. Add a `handle_disconnect` handler that sends the `terminated` event and exits the session cleanly. Document the `SourceLocation` span-storage rationale and refine the `SourceLine` resume-mode comment to be clearer about the "no source location → first mapped location" semantics. --- src/tools/miri/priroda/src/debugger.rs | 22 ++++++--- src/tools/miri/priroda/src/frontend/dap.rs | 46 ++++++++++++++++++- .../tests/ui/dap_scopes_variables_next.rs | 7 +++ .../tests/ui/dap_scopes_variables_next.stdin | 23 ++++++++++ .../tests/ui/dap_scopes_variables_next.stdout | 31 +++++++++++++ 5 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index b40ae12cdc396..557af87156b5f 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -14,7 +14,8 @@ use rustc_span::{Span, Symbol}; /// Structured source information for frontends. pub(super) struct SourceLocation { - // storing `span` to use it lazily to compute path. + // Keep the span so each frontend can resolve paths with its own rendering + // rules instead of forcing every caller to use one path representation. pub(super) span: Span, pub(super) line: usize, pub(super) column: usize, @@ -97,11 +98,15 @@ impl LocalDesc { enum ResumeMode { /// Stop at the next visible MIR instruction. MirInstruction, - /// Stop at the next source line + /// Stop at the next source line. /// - /// Take `Option` because some cases current state has no mapped to source code location + /// `None` means the current interpreter position has no source location, so + /// the first mapped source location is good enough to report. SourceLine(Option<(PathBuf, usize)>), /// Stop at the first mapped source location from a user-relevant frame. + /// + /// This is the DAP entry-stop primitive: it skips over interpreter startup + /// and Miri-internal frames until there is a location an editor can show. FirstUserSourceLocation, /// Continue until reaching a breakpoint. Continue, @@ -151,14 +156,17 @@ impl<'tcx> PrirodaContext<'tcx> { fn stepi(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::MirInstruction) } - fn step(&mut self) -> InterpResult<'tcx, StepResult> { + /// Step until the displayed source file or line changes. + pub(super) fn step(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::SourceLine(self.current_source_position())) } + /// Run until the initial editor-visible stop point. pub(super) fn stop_at_first_user_location(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::FirstUserSourceLocation) } + /// Return the active frame name while DAP still reports only one frame. pub(super) fn current_frame_name(&self) -> Option { let frame = self.ecx.active_thread_stack().last()?; Some(frame.instance().to_string()) @@ -205,12 +213,14 @@ impl<'tcx> PrirodaContext<'tcx> { ResumeMode::SourceLine(ref prev_location) => { match (prev_location, &self.current_location) { - // We started from an unmapped source location. Stop at the first mapped source location we can show to the user. + // We started from an unmapped location; stop once there + // is a source position the frontend can display. (None, Some(_)) => return interp_ok(StepResult::Step), (Some((prev_path, prev_line)), Some(current_location)) => { if let Some(current_path) = self.local_path(current_location) { - // A source step stops when the visible source position changes to a different file or line. + // A source step stops when the displayed source + // position changes to a different file or line. if *prev_path != current_path || *prev_line != current_location.line { return interp_ok(StepResult::Step); diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 01c3fbd2d5019..38906e0969f28 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -11,8 +11,10 @@ use emmy_dap_types::prelude::types::{ use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpResult, bug, interp_ok}; -use crate::debugger::{LocalDesc, PrirodaContext}; +use crate::debugger::{LocalDesc, PrirodaContext, StepResult}; +// Priroda still exposes one interpreted thread and one selected frame to DAP. +// Keep the ids stable so editor follow-up requests can address the stopped state. const THREAD_ID: i64 = 1; const STACK_FRAME_ID: i64 = 1; const LOCALS_VARIABLES_REFERENCE: i64 = 1; @@ -116,8 +118,21 @@ impl DapSession { interp_ok( self.handle_variables(request, session).map(|()| DispatchOutcome::Continue), ), + Command::Next(_) | Command::StepIn(_) => { + let body = match &request.command { + Command::Next(_) => ResponseBody::Next, + Command::StepIn(_) => ResponseBody::StepIn, + _ => unreachable!(), + }; + let res = self.handle_step(request, body, session)?; + interp_ok(res.map(|()| DispatchOutcome::Continue)) + } + Command::Disconnect(_) => + interp_ok(self.handle_disconnect(request).map(|()| DispatchOutcome::Exit)), _ => - interp_ok(self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit)), + interp_ok( + self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + ), } } @@ -253,6 +268,26 @@ impl DapSession { Ok(()) } + /// FIXME: distinguish step-over from step-in once Priroda has call-aware stepping. + fn handle_step<'tcx>( + &mut self, + request: Request, + body: ResponseBody, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, ServerResult> { + let result = session.step()?; + interp_ok( + self.server + .respond(request.success(body)) + .and_then(|()| self.send_stopped_event(Self::stopped_reason(result))), + ) + } + + fn handle_disconnect(&mut self, request: Request) -> ServerResult { + self.server.respond(request.success(ResponseBody::Disconnect))?; + self.server.send_event(Event::Terminated(None)) + } + fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { eprintln!( "priroda dap: unsupported request during DAP demo milestone: {}", @@ -274,6 +309,13 @@ impl DapSession { })) } + fn stopped_reason(result: StepResult) -> StoppedEventReason { + match result { + StepResult::Step => StoppedEventReason::Step, + StepResult::Breakpoint => StoppedEventReason::Breakpoint, + } + } + fn display_command(command: &Command) -> &'static str { match command { Command::Initialize(_) => "initialize", diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.rs b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.rs new file mode 100644 index 0000000000000..081c3ce1d97c6 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.rs @@ -0,0 +1,7 @@ +//@ compile-flags: --dap + +fn main() { + let x = 1_i32; + let y = true; + let _ = (x, y); +} diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdin b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdin new file mode 100644 index 0000000000000..40da18a5832da --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdin @@ -0,0 +1,23 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 76 + +{"seq":4,"type":"request","command":"stackTrace","arguments":{"threadId":1}}Content-Length: 71 + +{"seq":5,"type":"request","command":"scopes","arguments":{"frameId":1}}Content-Length: 85 + +{"seq":6,"type":"request","command":"variables","arguments":{"variablesReference":1}}Content-Length: 70 + +{"seq":7,"type":"request","command":"next","arguments":{"threadId":1}}Content-Length: 76 + +{"seq":8,"type":"request","command":"stackTrace","arguments":{"threadId":1}}Content-Length: 71 + +{"seq":9,"type":"request","command":"scopes","arguments":{"frameId":1}}Content-Length: 86 + +{"seq":10,"type":"request","command":"variables","arguments":{"variablesReference":1}}Content-Length: 65 + +{"seq":11,"type":"request","command":"disconnect","arguments":{}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout new file mode 100644 index 0000000000000..3620bd1a0b9cb --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout @@ -0,0 +1,31 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 + +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 314 + +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs"},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 + +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 + +{"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: 88 + +{"seq":9,"type":"response","request_seq":7,"success":true,"command":"next","error":null}Content-Length: 186 + +{"seq":10,"type":"event","event":"stopped","body":{"reason":"step","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 315 + +{"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs"},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: 219 + +{"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 528 + +{"seq":13,"type":"response","request_seq":10,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"1_i32","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: 96 + +{"seq":14,"type":"response","request_seq":11,"success":true,"command":"disconnect","error":null}Content-Length: 58 + +{"seq":15,"type":"event","event":"terminated","body":null} \ No newline at end of file From cb35d76fe95ad4746123e78eecc93cd68576628a Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:35:35 +0300 Subject: [PATCH 20/42] [Priroda] Return Continue instead of Exit for unsupported DAP requests When the session receives an unsupported DAP request, return `DispatchOutcome::Continue` instead of `Exit` so the debug adapter keeps running after sending the error response. Remove the `eprintln!` side channel from `handle_unsupported_request` since the framed DAP error response is the single authoritative error-reporting path. Include the command name in the error message string so the DAP client sees which request was rejected. --- src/tools/miri/priroda/src/frontend/dap.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 38906e0969f28..40012acec64c2 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -93,7 +93,7 @@ impl DapSession { // client gets a framed error. if !self.initialized && !matches!(&request.command, Command::Initialize(_)) { return interp_ok( - self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + self.handle_unsupported_request(request).map(|()| DispatchOutcome::Continue), ); } @@ -131,7 +131,7 @@ impl DapSession { interp_ok(self.handle_disconnect(request).map(|()| DispatchOutcome::Exit)), _ => interp_ok( - self.handle_unsupported_request(request).map(|()| DispatchOutcome::Exit), + self.handle_unsupported_request(request).map(|()| DispatchOutcome::Continue), ), } } @@ -289,11 +289,11 @@ impl DapSession { } fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { - eprintln!( - "priroda dap: unsupported request during DAP demo milestone: {}", + let message = format!( + "unsupported request in Priroda DAP demo mode: {}", Self::display_command(&request.command) ); - let response = request.error("unsupported request in Priroda DAP demo mode"); + let response = request.error(&message); self.server.respond(response) } From f1fb454db5b85ab66ad04f46b8adb0aacbd9724f Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 09:05:41 +0300 Subject: [PATCH 21/42] [Priroda] Document DAP prototype in README --- src/tools/miri/priroda/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tools/miri/priroda/README.md b/src/tools/miri/priroda/README.md index a8c25bf279868..6283bc28bb7c2 100644 --- a/src/tools/miri/priroda/README.md +++ b/src/tools/miri/priroda/README.md @@ -38,6 +38,18 @@ from `miri/priroda/`: cargo run -- ../tests/pass/empty_main.rs ``` +## DAP Prototype + +Priroda's `--dap` mode speaks a bounded Debug Adapter Protocol prototype over +stdio. It currently supports the startup handshake, stops at the first +user-relevant source location after `configurationDone`, reports one current +stack frame, exposes one flat Locals scope, and maps `list_locals()` into DAP +variables with no child expansion. + +The `next` and `stepIn` requests are wired to Priroda's existing source-line +step so VS Code can drive one visible step. They are not true DAP step-over or +step-in semantics yet. + ## Test Priroda's CLI tests also need `MIRI_SYSROOT`. Run them from `miri/priroda/`: From afd33c80716d1d8422f9f7e6fd3af58fdf49d883 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 07:56:18 +0300 Subject: [PATCH 22/42] [Priroda] Translate interpreter exits into DAP events --- src/tools/miri/priroda/src/frontend/dap.rs | 84 ++++++++++++++++++---- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 40012acec64c2..2c4f41e4fb732 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,6 +1,6 @@ use std::io::{self, BufReader, BufWriter}; -use emmy_dap_types::prelude::events::StoppedEventBody; +use emmy_dap_types::prelude::events::{ExitedEventBody, StoppedEventBody}; use emmy_dap_types::prelude::responses::{ ScopesResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, }; @@ -9,7 +9,7 @@ use emmy_dap_types::prelude::types::{ Variable, }; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; -use miri::{InterpResult, bug, interp_ok}; +use miri::{InterpErrorInfo, InterpErrorKind, InterpResult, TerminationInfo, bug, interp_ok}; use crate::debugger::{LocalDesc, PrirodaContext, StepResult}; @@ -26,6 +26,12 @@ enum DispatchOutcome { Exit, } +enum ExecutionOutcome { + Stopped(StepResult), + Terminated { code: i32 }, + Failed(String), +} + /// Debug Adapter Protocol frontend. pub(crate) struct Dap; @@ -186,13 +192,20 @@ impl DapSession { request: Request, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { - session.stop_at_first_user_location()?; - let response = request.success(ResponseBody::ConfigurationDone); - interp_ok( - self.server - .respond(response) - .and_then(|()| self.send_stopped_event(StoppedEventReason::Entry)), - ) + match Self::execution_outcome(session.stop_at_first_user_location()) { + ExecutionOutcome::Stopped(_) => { + let response = request.success(ResponseBody::ConfigurationDone); + interp_ok( + self.server + .respond(response) + .and_then(|()| self.send_stopped_event(StoppedEventReason::Entry)), + ) + } + ExecutionOutcome::Terminated { code } => + interp_ok(self.respond_terminated(request, ResponseBody::ConfigurationDone, code)), + ExecutionOutcome::Failed(message) => + interp_ok(self.respond_execution_error(request, message)), + } } /// FIXME: replace this with Miri thread state once Priroda exposes a @@ -275,12 +288,18 @@ impl DapSession { body: ResponseBody, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { - let result = session.step()?; - interp_ok( - self.server - .respond(request.success(body)) - .and_then(|()| self.send_stopped_event(Self::stopped_reason(result))), - ) + match Self::execution_outcome(session.step()) { + ExecutionOutcome::Stopped(result) => + interp_ok( + self.server + .respond(request.success(body)) + .and_then(|()| self.send_stopped_event(Self::stopped_reason(result))), + ), + ExecutionOutcome::Terminated { code } => + interp_ok(self.respond_terminated(request, body, code)), + ExecutionOutcome::Failed(message) => + interp_ok(self.respond_execution_error(request, message)), + } } fn handle_disconnect(&mut self, request: Request) -> ServerResult { @@ -297,6 +316,41 @@ impl DapSession { self.server.respond(response) } + fn respond_execution_error(&mut self, request: Request, message: String) -> ServerResult { + self.server.respond(request.error(&message))?; + self.server.send_event(Event::Terminated(None)) + } + + fn respond_terminated( + &mut self, + request: Request, + body: ResponseBody, + code: i32, + ) -> ServerResult { + self.server.respond(request.success(body))?; + self.server.send_event(Event::Exited(ExitedEventBody { exit_code: code.into() }))?; + self.server.send_event(Event::Terminated(None))?; + Ok(()) + } + + fn execution_outcome<'tcx>(result: InterpResult<'tcx, StepResult>) -> ExecutionOutcome { + match result.report_err() { + Ok(step) => ExecutionOutcome::Stopped(step), + Err(err) => Self::interp_error_outcome(err), + } + } + + fn interp_error_outcome<'tcx>(err: InterpErrorInfo<'tcx>) -> ExecutionOutcome { + let kind = err.into_kind(); + if let InterpErrorKind::MachineStop(info) = &kind + && let Some(TerminationInfo::Exit { code, .. }) = info.downcast_ref::() + { + return ExecutionOutcome::Terminated { code: *code }; + } + + ExecutionOutcome::Failed(kind.to_string()) + } + fn send_stopped_event(&mut self, reason: StoppedEventReason) -> ServerResult { self.server.send_event(Event::Stopped(StoppedEventBody { reason, From dc93370b69d7cabb5d2bc720827ab587b9f6ef7b Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 08:49:41 +0300 Subject: [PATCH 23/42] [Priroda] Track DAP lifecycle state and validate request ids --- src/tools/miri/priroda/src/frontend/dap.rs | 223 ++++++++++++++++-- .../dap_rejects_non_initialize_first.stderr | 1 - .../dap_rejects_non_initialize_first.stdout | 4 +- 3 files changed, 203 insertions(+), 25 deletions(-) delete mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 2c4f41e4fb732..dcc497abe8b45 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -26,6 +26,15 @@ enum DispatchOutcome { Exit, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum DapState { + Fresh, + Initialized, + Launched, + Stopped, + Terminated, +} + enum ExecutionOutcome { Stopped(StepResult), Terminated { code: i32 }, @@ -55,7 +64,7 @@ type DapServer = Server, io::StdoutLock<'static>>; /// Owns the DAP stdio transport and dispatches requests into Priroda handlers. struct DapSession { server: DapServer, - initialized: bool, + state: DapState, } impl DapSession { @@ -65,7 +74,7 @@ impl DapSession { BufReader::new(io::stdin().lock()), BufWriter::new(io::stdout().lock()), ), - initialized: false, + state: DapState::Fresh, } } @@ -95,11 +104,10 @@ impl DapSession { request: Request, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { - // Reject non-initialize requests before the handshake completes so the - // client gets a framed error. - if !self.initialized && !matches!(&request.command, Command::Initialize(_)) { + if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { return interp_ok( - self.handle_unsupported_request(request).map(|()| DispatchOutcome::Continue), + self.respond_error(request, "initialize must be sent first") + .map(|()| DispatchOutcome::Continue), ); } @@ -110,7 +118,7 @@ impl DapSession { interp_ok(self.handle_launch(request).map(|()| DispatchOutcome::Continue)), Command::ConfigurationDone => { let res = self.handle_configuration_done(request, session)?; - interp_ok(res.map(|()| DispatchOutcome::Continue)) + interp_ok(res.map(|()| self.dispatch_outcome())) } Command::Threads => interp_ok(self.handle_threads(request).map(|()| DispatchOutcome::Continue)), @@ -131,7 +139,7 @@ impl DapSession { _ => unreachable!(), }; let res = self.handle_step(request, body, session)?; - interp_ok(res.map(|()| DispatchOutcome::Continue)) + interp_ok(res.map(|()| self.dispatch_outcome())) } Command::Disconnect(_) => interp_ok(self.handle_disconnect(request).map(|()| DispatchOutcome::Exit)), @@ -144,8 +152,16 @@ impl DapSession { /// FIXME: connect launch arguments to Priroda's session model. fn handle_launch(&mut self, request: Request) -> ServerResult { + if self.reject_after_termination(&request)? + || self.require_state(&request, DapState::Initialized, "launch requires initialize")? + { + return Ok(()); + } + let response = request.success(ResponseBody::Launch); - self.server.respond(response) + self.server.respond(response)?; + self.state = DapState::Launched; + Ok(()) } fn handle_scopes<'tcx>( @@ -153,6 +169,13 @@ impl DapSession { request: Request, _session: &PrirodaContext<'tcx>, ) -> ServerResult { + if self.reject_after_termination(&request)? + || self.require_stopped(&request)? + || self.require_frame_id(&request)? + { + return Ok(()); + } + let response = request.success(ResponseBody::Scopes(ScopesResponse { scopes: vec![Scope { name: "Locals".to_string(), @@ -176,10 +199,16 @@ impl DapSession { request: Request, session: &PrirodaContext<'tcx>, ) -> ServerResult { + if self.reject_after_termination(&request)? + || self.require_stopped(&request)? + || self.require_variables_reference(&request)? + { + return Ok(()); + } + let variables = match &request.command { - Command::Variables(args) if args.variables_reference == LOCALS_VARIABLES_REFERENCE => + Command::Variables(_) => session.list_locals().into_iter().map(Self::local_to_variable).collect(), - Command::Variables(_) => Vec::new(), _ => unreachable!(), }; @@ -192,14 +221,21 @@ impl DapSession { request: Request, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { + let rejected = match self.check_configuration_done_request(&request) { + Ok(rejected) => rejected, + Err(err) => return interp_ok(Err(err)), + }; + if rejected { + return interp_ok(Ok(())); + } + match Self::execution_outcome(session.stop_at_first_user_location()) { ExecutionOutcome::Stopped(_) => { let response = request.success(ResponseBody::ConfigurationDone); - interp_ok( - self.server - .respond(response) - .and_then(|()| self.send_stopped_event(StoppedEventReason::Entry)), - ) + interp_ok(self.server.respond(response).and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(StoppedEventReason::Entry) + })) } ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, ResponseBody::ConfigurationDone, code)), @@ -211,6 +247,10 @@ impl DapSession { /// FIXME: replace this with Miri thread state once Priroda exposes a /// frontend-facing thread model. fn handle_threads(&mut self, request: Request) -> ServerResult { + if self.reject_after_termination(&request)? { + return Ok(()); + } + let response = request.success(ResponseBody::Threads(ThreadsResponse { threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], })); @@ -223,6 +263,13 @@ impl DapSession { request: Request, session: &PrirodaContext<'tcx>, ) -> ServerResult { + if self.reject_after_termination(&request)? + || self.require_stopped(&request)? + || self.require_thread_id(&request)? + { + return Ok(()); + } + let stack_frames = match &session.current_location { Some(location) => { let path = session.local_path(location); @@ -271,13 +318,20 @@ impl DapSession { fn handle_initialize(&mut self, request: Request) -> ServerResult { // Advertise configurationDone support ahead of its handler so VS Code // completes the full handshake; the handler arrives in a later commit. + if self.reject_after_termination(&request)? { + return Ok(()); + } + if self.state != DapState::Fresh { + return self.respond_error(request, "initialize may only be sent once"); + } + let response = request.success(ResponseBody::Initialize(Capabilities { supports_configuration_done_request: Some(true), ..Capabilities::default() })); self.server.respond(response)?; self.server.send_event(Event::Initialized)?; - self.initialized = true; + self.state = DapState::Initialized; Ok(()) } @@ -288,13 +342,20 @@ impl DapSession { body: ResponseBody, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { + let rejected = match self.check_step_request(&request) { + Ok(rejected) => rejected, + Err(err) => return interp_ok(Err(err)), + }; + if rejected { + return interp_ok(Ok(())); + } + match Self::execution_outcome(session.step()) { ExecutionOutcome::Stopped(result) => - interp_ok( - self.server - .respond(request.success(body)) - .and_then(|()| self.send_stopped_event(Self::stopped_reason(result))), - ), + interp_ok(self.server.respond(request.success(body)).and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(Self::stopped_reason(result)) + })), ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, body, code)), ExecutionOutcome::Failed(message) => @@ -304,6 +365,7 @@ impl DapSession { fn handle_disconnect(&mut self, request: Request) -> ServerResult { self.server.respond(request.success(ResponseBody::Disconnect))?; + self.state = DapState::Terminated; self.server.send_event(Event::Terminated(None)) } @@ -316,7 +378,123 @@ impl DapSession { self.server.respond(response) } + fn reject_after_termination(&mut self, request: &Request) -> ServerResult { + if self.state == DapState::Terminated { + self.server.respond(request.clone().error("request received after termination"))?; + return Ok(true); + } + + Ok(false) + } + + fn require_state( + &mut self, + request: &Request, + expected: DapState, + message: &'static str, + ) -> ServerResult { + if self.state != expected { + self.server.respond(request.clone().error(message))?; + return Ok(true); + } + + Ok(false) + } + + fn require_stopped(&mut self, request: &Request) -> ServerResult { + if self.state != DapState::Stopped { + self.server.respond(request.clone().error("request requires a stopped frame"))?; + return Ok(true); + } + + Ok(false) + } + + fn require_thread_id(&mut self, request: &Request) -> ServerResult { + let valid = match &request.command { + Command::StackTrace(args) => args.thread_id == THREAD_ID, + Command::Next(args) => args.thread_id == THREAD_ID, + Command::StepIn(args) => args.thread_id == THREAD_ID, + _ => unreachable!(), + }; + + if !valid { + self.server.respond(request.clone().error("unknown threadId"))?; + return Ok(true); + } + + Ok(false) + } + + fn require_frame_id(&mut self, request: &Request) -> ServerResult { + let Command::Scopes(args) = &request.command else { + unreachable!(); + }; + + if args.frame_id != STACK_FRAME_ID { + self.server.respond(request.clone().error("unknown frameId"))?; + return Ok(true); + } + + Ok(false) + } + + fn require_variables_reference(&mut self, request: &Request) -> ServerResult { + let Command::Variables(args) = &request.command else { + unreachable!(); + }; + + if args.variables_reference != LOCALS_VARIABLES_REFERENCE { + self.server.respond(request.clone().error("unknown variablesReference"))?; + return Ok(true); + } + + Ok(false) + } + + fn check_configuration_done_request(&mut self, request: &Request) -> ServerResult { + if self.reject_after_termination(request)? { + return Ok(true); + } + + if self.state == DapState::Stopped { + self.server + .respond(request.clone().error("configurationDone may only be sent once"))?; + return Ok(true); + } + + if self.require_state(request, DapState::Launched, "configurationDone requires launch")? { + return Ok(true); + } + + Ok(false) + } + + fn check_step_request(&mut self, request: &Request) -> ServerResult { + if self.reject_after_termination(request)? + || self.require_stopped(request)? + || self.require_thread_id(request)? + { + return Ok(true); + } + + Ok(false) + } + + fn respond_error(&mut self, request: Request, message: &str) -> ServerResult { + self.server.respond(request.error(message)) + } + + fn dispatch_outcome(&self) -> DispatchOutcome { + if self.state == DapState::Terminated { + DispatchOutcome::Exit + } else { + DispatchOutcome::Continue + } + } + fn respond_execution_error(&mut self, request: Request, message: String) -> ServerResult { + self.state = DapState::Terminated; self.server.respond(request.error(&message))?; self.server.send_event(Event::Terminated(None)) } @@ -327,6 +505,7 @@ impl DapSession { body: ResponseBody, code: i32, ) -> ServerResult { + self.state = DapState::Terminated; self.server.respond(request.success(body))?; self.server.send_event(Event::Exited(ExitedEventBody { exit_code: code.into() }))?; self.server.send_event(Event::Terminated(None))?; diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr deleted file mode 100644 index 2641eb804868c..0000000000000 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stderr +++ /dev/null @@ -1 +0,0 @@ -priroda dap: unsupported request during DAP demo milestone: next diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout index 18ed56545ac9d..55ba4e862250f 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout @@ -1,3 +1,3 @@ -Content-Length: 146 +Content-Length: 131 -{"seq":1,"type":"response","request_seq":2,"success":false,"message":"unsupported request in Priroda DAP demo mode","command":"next","error":null} \ No newline at end of file +{"seq":1,"type":"response","request_seq":2,"success":false,"message":"initialize must be sent first","command":"next","error":null} \ No newline at end of file From 51dbde5e56d473862979da4ea4cec95e96c0b8a5 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 07:56:18 +0300 Subject: [PATCH 24/42] [Priroda] Add DAP negative protocol fixtures --- ...ejects_configuration_done_before_launch.rs | 3 +++ ...cts_configuration_done_before_launch.stdin | 7 ++++++ ...ts_configuration_done_before_launch.stdout | 11 ++++++++ ..._rejects_next_before_configuration_done.rs | 3 +++ ...jects_next_before_configuration_done.stdin | 9 +++++++ ...ects_next_before_configuration_done.stdout | 13 ++++++++++ ...dap_rejects_repeated_configuration_done.rs | 3 +++ ..._rejects_repeated_configuration_done.stdin | 11 ++++++++ ...rejects_repeated_configuration_done.stdout | 17 +++++++++++++ .../priroda/tests/ui/dap_rejects_wrong_ids.rs | 6 +++++ .../tests/ui/dap_rejects_wrong_ids.stdin | 19 ++++++++++++++ .../tests/ui/dap_rejects_wrong_ids.stdout | 25 +++++++++++++++++++ 12 files changed, 127 insertions(+) create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.rs create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdin create mode 100644 src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin new file mode 100644 index 0000000000000..c1dedb5404eca --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin @@ -0,0 +1,7 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 56 + +{"seq":2,"type":"request","command":"configurationDone"}Content-Length: 64 + +{"seq":3,"type":"request","command":"disconnect","arguments":{}} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout new file mode 100644 index 0000000000000..1e3a55ff64ef5 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout @@ -0,0 +1,11 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 148 + +{"seq":3,"type":"response","request_seq":2,"success":false,"message":"configurationDone requires launch","command":"configurationDone","error":null}Content-Length: 94 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"disconnect","error":null}Content-Length: 57 + +{"seq":5,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin new file mode 100644 index 0000000000000..a582d4adc7fdb --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin @@ -0,0 +1,9 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 70 + +{"seq":3,"type":"request","command":"next","arguments":{"threadId":1}}Content-Length: 65 + +{"seq":4,"type":"request","command":"disconnect","arguments":{}} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout new file mode 100644 index 0000000000000..69906b4578351 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout @@ -0,0 +1,13 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 134 + +{"seq":4,"type":"response","request_seq":3,"success":false,"message":"request requires a stopped frame","command":"next","error":null}Content-Length: 94 + +{"seq":5,"type":"response","request_seq":4,"success":true,"command":"disconnect","error":null}Content-Length: 57 + +{"seq":6,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs new file mode 100644 index 0000000000000..c1f1ed6f67bea --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin new file mode 100644 index 0000000000000..d98039165e4d8 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin @@ -0,0 +1,11 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 56 + +{"seq":4,"type":"request","command":"configurationDone"}Content-Length: 65 + +{"seq":5,"type":"request","command":"disconnect","arguments":{}} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout new file mode 100644 index 0000000000000..d81c109466e4c --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout @@ -0,0 +1,17 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 + +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 154 + +{"seq":6,"type":"response","request_seq":4,"success":false,"message":"configurationDone may only be sent once","command":"configurationDone","error":null}Content-Length: 94 + +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"disconnect","error":null}Content-Length: 57 + +{"seq":8,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.rs b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.rs new file mode 100644 index 0000000000000..cd7ad8e0bb32f --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.rs @@ -0,0 +1,6 @@ +//@ compile-flags: --dap + +fn main() { + let x = 1_i32; + let _ = x; +} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdin b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdin new file mode 100644 index 0000000000000..a2a9dd1595bc8 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdin @@ -0,0 +1,19 @@ +Content-Length: 85 + +{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"priroda"}}Content-Length: 60 + +{"seq":2,"type":"request","command":"launch","arguments":{}}Content-Length: 56 + +{"seq":3,"type":"request","command":"configurationDone"}Content-Length: 76 + +{"seq":4,"type":"request","command":"stackTrace","arguments":{"threadId":2}}Content-Length: 71 + +{"seq":5,"type":"request","command":"scopes","arguments":{"frameId":2}}Content-Length: 85 + +{"seq":6,"type":"request","command":"variables","arguments":{"variablesReference":2}}Content-Length: 70 + +{"seq":7,"type":"request","command":"next","arguments":{"threadId":2}}Content-Length: 72 + +{"seq":8,"type":"request","command":"stepIn","arguments":{"threadId":2}}Content-Length: 65 + +{"seq":9,"type":"request","command":"disconnect","arguments":{}} diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout new file mode 100644 index 0000000000000..0230fe0e3e1c4 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout @@ -0,0 +1,25 @@ +Content-Length: 143 + +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 + +{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 + +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 + +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 + +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 101 + +{"seq":6,"type":"response","request_seq":4,"success":false,"message":"unknown threadId","error":null}Content-Length: 100 + +{"seq":7,"type":"response","request_seq":5,"success":false,"message":"unknown frameId","error":null}Content-Length: 111 + +{"seq":8,"type":"response","request_seq":6,"success":false,"message":"unknown variablesReference","error":null}Content-Length: 118 + +{"seq":9,"type":"response","request_seq":7,"success":false,"message":"unknown threadId","command":"next","error":null}Content-Length: 121 + +{"seq":10,"type":"response","request_seq":8,"success":false,"message":"unknown threadId","command":"stepIn","error":null}Content-Length: 95 + +{"seq":11,"type":"response","request_seq":9,"success":true,"command":"disconnect","error":null}Content-Length: 58 + +{"seq":12,"type":"event","event":"terminated","body":null} \ No newline at end of file From 00c51d428cf1f43f5d1c5aee74e0154cbbbe50ff Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:44:54 +0300 Subject: [PATCH 25/42] [Priroda] Use bug! for dispatch-guaranteed DAP invariants Replace `unreachable!()` with `bug!(...)` at the four dispatch-guaranteed invariant sites so they produce a meaningful message when the guard fails instead of a bare panic. Also switch the DAP error print to Debug format so transport errors include their chain. --- src/tools/miri/priroda/src/frontend/dap.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index dcc497abe8b45..b4b92018e1653 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -52,7 +52,7 @@ impl Dap { ) -> InterpResult<'tcx> { // FIXME: make this unbounded once Priroda has a full session lifecycle. if let Err(err) = DapSession::stdio().run_requests(session)? { - eprintln!("priroda dap error: {err}"); + eprintln!("priroda dap error: {err:?}"); } interp_ok(()) @@ -136,7 +136,7 @@ impl DapSession { let body = match &request.command { Command::Next(_) => ResponseBody::Next, Command::StepIn(_) => ResponseBody::StepIn, - _ => unreachable!(), + _ => bug!("step body is selected by the outer Next/StepIn match"), }; let res = self.handle_step(request, body, session)?; interp_ok(res.map(|()| self.dispatch_outcome())) @@ -209,7 +209,7 @@ impl DapSession { let variables = match &request.command { Command::Variables(_) => session.list_locals().into_iter().map(Self::local_to_variable).collect(), - _ => unreachable!(), + _ => bug!("dispatch routes only Variables to handle_variables"), }; let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); @@ -428,7 +428,7 @@ impl DapSession { fn require_frame_id(&mut self, request: &Request) -> ServerResult { let Command::Scopes(args) = &request.command else { - unreachable!(); + bug!("dispatch routes only scopes to require_frame_id"); }; if args.frame_id != STACK_FRAME_ID { @@ -441,7 +441,7 @@ impl DapSession { fn require_variables_reference(&mut self, request: &Request) -> ServerResult { let Command::Variables(args) = &request.command else { - unreachable!(); + bug!("dispatch routes only variables to require_variables_reference"); }; if args.variables_reference != LOCALS_VARIABLES_REFERENCE { From f32ac7c2d097063ae5d66f0b88453c7a0e2bf42e Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:45:15 +0300 Subject: [PATCH 26/42] [Priroda] Resolve macro-backed spans to their callsite Call `span.source_callsite()` in `resolve_current_location` so breakpoints and source reporting use the user-visible macro call site instead of the expanded macro body for lines generated by `println!`, `assert_eq!`, and similar macros. --- src/tools/miri/priroda/src/debugger.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index 557af87156b5f..07c9ddd2807fa 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -310,13 +310,12 @@ impl<'tcx> PrirodaContext<'tcx> { } fn resolve_current_location(&self) -> Option { - // FIXME: resolve macro-backed lines such as `println!` and `assert_eq!` - // through `span.source_callsite()` before matching breakpoints. let span = self.ecx.machine.current_user_relevant_span(); if span.is_dummy() { return None; } + let span = span.source_callsite(); let source_map = self.ecx.tcx.sess.source_map(); let loc = source_map.lookup_char_pos(span.lo()); From 1f184d7e966afe62a143fccf95857a1107230624 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:24:32 +0300 Subject: [PATCH 27/42] [Priroda] Handle DAP continue request Add the `continue` command handler, reusing the existing source-line stepping and breakpoint infrastructure. The `handle_continue` method follows the same `ExecutionOutcome` dispatch pattern as `handle_step`. Include `Command::Continue` in `require_thread_id` validation so the request passes the thread-id guard, and widen the fallback from `unreachable!()` to `true` so any future request with a thread-id field passes validation rather than panicking. --- src/tools/miri/priroda/src/debugger.rs | 2 +- src/tools/miri/priroda/src/frontend/dap.rs | 37 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index 07c9ddd2807fa..93fa8c2299dc0 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -173,7 +173,7 @@ impl<'tcx> PrirodaContext<'tcx> { } /// Continue execution until reaching a breakpoint or propagating termination. - fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { + pub(super) fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { self.resume(ResumeMode::Continue) } diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index b4b92018e1653..1d1caab50c664 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -2,7 +2,7 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::events::{ExitedEventBody, StoppedEventBody}; use emmy_dap_types::prelude::responses::{ - ScopesResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, + ContinueResponse, ScopesResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, }; use emmy_dap_types::prelude::types::{ Capabilities, Scope, ScopePresentationhint, Source, StackFrame, StoppedEventReason, Thread, @@ -132,6 +132,10 @@ impl DapSession { interp_ok( self.handle_variables(request, session).map(|()| DispatchOutcome::Continue), ), + Command::Continue(_) => { + let res = self.handle_continue(request, session)?; + interp_ok(res.map(|()| self.dispatch_outcome())) + } Command::Next(_) | Command::StepIn(_) => { let body = match &request.command { Command::Next(_) => ResponseBody::Next, @@ -363,6 +367,34 @@ impl DapSession { } } + fn handle_continue<'tcx>( + &mut self, + request: Request, + session: &mut PrirodaContext<'tcx>, + ) -> InterpResult<'tcx, ServerResult> { + let rejected = match self.check_step_request(&request) { + Ok(rejected) => rejected, + Err(err) => return interp_ok(Err(err)), + }; + if rejected { + return interp_ok(Ok(())); + } + + let body = ResponseBody::Continue(ContinueResponse { all_threads_continued: Some(true) }); + + match Self::execution_outcome(session.continue_execution()) { + ExecutionOutcome::Stopped(result) => + interp_ok(self.server.respond(request.success(body)).and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(Self::stopped_reason(result)) + })), + ExecutionOutcome::Terminated { code } => + interp_ok(self.respond_terminated(request, body, code)), + ExecutionOutcome::Failed(message) => + interp_ok(self.respond_execution_error(request, message)), + } + } + fn handle_disconnect(&mut self, request: Request) -> ServerResult { self.server.respond(request.success(ResponseBody::Disconnect))?; self.state = DapState::Terminated; @@ -415,7 +447,8 @@ impl DapSession { Command::StackTrace(args) => args.thread_id == THREAD_ID, Command::Next(args) => args.thread_id == THREAD_ID, Command::StepIn(args) => args.thread_id == THREAD_ID, - _ => unreachable!(), + Command::Continue(args) => args.thread_id == THREAD_ID, + _ => true, }; if !valid { From affac58e3fd0b4f0746d9077b8eab0dab843039c Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:26:45 +0300 Subject: [PATCH 28/42] [Priroda] Handle DAP setBreakpoints request Add the `setBreakpoints` command handler that maps DAP source breakpoints to the shared `PrirodaContext::set_breakpoint` breakpoint table. Every requested breakpoint is marked as verified so that VS Code displays the breakpoint marker in the editor gutter; path and line-range validation is deferred per the existing FIXME in `debugger.rs`. --- src/tools/miri/priroda/src/debugger.rs | 3 +- src/tools/miri/priroda/src/frontend/dap.rs | 51 ++++++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index 93fa8c2299dc0..aca596dc3f18e 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -177,7 +177,7 @@ impl<'tcx> PrirodaContext<'tcx> { self.resume(ResumeMode::Continue) } - fn set_breakpoint(&mut self, path: PathBuf, line: usize) -> BreakpointSetResult { + pub(super) fn set_breakpoint(&mut self, path: PathBuf, line: usize) -> BreakpointSetResult { // FIXME: validate breakpoints here so every frontend gets the same behavior. // Reject empty paths, missing files, directories, and line 0. Decide whether // out-of-range lines should be rejected or kept as pending breakpoints. @@ -305,7 +305,6 @@ impl<'tcx> PrirodaContext<'tcx> { fn current_breakpoint(&self) -> Option<(PathBuf, usize)> { let (path, line) = self.current_source_position()?; let lines = self.breakpoints.get(&path)?; - if lines.contains(&line) { Some((path, line)) } else { None } } diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 1d1caab50c664..2e76ccdbb2a47 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -2,11 +2,12 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::events::{ExitedEventBody, StoppedEventBody}; use emmy_dap_types::prelude::responses::{ - ContinueResponse, ScopesResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, + ContinueResponse, ScopesResponse, SetBreakpointsResponse, StackTraceResponse, ThreadsResponse, + VariablesResponse, }; use emmy_dap_types::prelude::types::{ - Capabilities, Scope, ScopePresentationhint, Source, StackFrame, StoppedEventReason, Thread, - Variable, + Breakpoint as DapBreakpoint, Capabilities, Scope, ScopePresentationhint, Source, StackFrame, + StoppedEventReason, Thread, Variable, }; use emmy_dap_types::prelude::{Command, Event, Request, ResponseBody, Server}; use miri::{InterpErrorInfo, InterpErrorKind, InterpResult, TerminationInfo, bug, interp_ok}; @@ -136,6 +137,11 @@ impl DapSession { let res = self.handle_continue(request, session)?; interp_ok(res.map(|()| self.dispatch_outcome())) } + Command::SetBreakpoints(_) => + interp_ok( + self.handle_set_breakpoints(request, session) + .map(|()| DispatchOutcome::Continue), + ), Command::Next(_) | Command::StepIn(_) => { let body = match &request.command { Command::Next(_) => ResponseBody::Next, @@ -395,6 +401,45 @@ impl DapSession { } } + fn handle_set_breakpoints<'tcx>( + &mut self, + request: Request, + session: &mut PrirodaContext<'tcx>, + ) -> ServerResult { + if self.reject_after_termination(&request)? { + return Ok(()); + } + + let mut breakpoints = Vec::new(); + if let Command::SetBreakpoints(ref args) = request.command { + if let Some(ref path_str) = args.source.path { + let path = std::path::PathBuf::from(path_str); + if let Some(ref req_bps) = args.breakpoints { + for req_bp in req_bps { + let line = req_bp.line as usize; + session.set_breakpoint(path.clone(), line); + breakpoints.push(DapBreakpoint { + verified: true, + message: None, + source: Some(args.source.clone()), + line: Some(req_bp.line), + column: req_bp.column, + end_line: None, + end_column: None, + id: None, + instruction_reference: None, + offset: None, + }); + } + } + } + } + + let response = + request.success(ResponseBody::SetBreakpoints(SetBreakpointsResponse { breakpoints })); + self.server.respond(response) + } + fn handle_disconnect(&mut self, request: Request) -> ServerResult { self.server.respond(request.success(ResponseBody::Disconnect))?; self.state = DapState::Terminated; From 042a542512c4ed2d6532236d6c5e098130e66b52 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 2 Aug 2026 19:28:19 +0300 Subject: [PATCH 29/42] [Priroda] Advertise single-thread execution + unbounded loop Mark `supportsSingleThreadExecutionRequests: true` so that VS Code sends the `singleThread` flag on step/continue requests. This lets the editor drive the single-threaded prototype without protocol errors. Remove the `MAX_REQUEST_COUNT` guard and the `for 0..MAX_REQUEST_COUNT` loop, replacing them with a simple `loop {}`. The debug adapter now handles an unbounded number of requests, terminating only on disconnect or an explicit exit event. Set `source_reference: Some(0)` on stack frames so the editor does not request source content through `source` requests: the file is on disk and the editor can read it directly. Update all DAP `.stdout` fixture files to reflect the new capability field in the `initialize` response body. --- src/tools/miri/priroda/src/frontend/dap.rs | 9 +++------ .../miri/priroda/tests/ui/dap_initialize.stdout | 4 ++-- .../priroda/tests/ui/dap_initialize_launch.stdout | 4 ++-- .../dap_initialize_launch_configuration_done.stdout | 4 ++-- ...p_rejects_configuration_done_before_launch.stdout | 4 ++-- ...dap_rejects_next_before_configuration_done.stdout | 4 ++-- .../dap_rejects_repeated_configuration_done.stdout | 4 ++-- .../priroda/tests/ui/dap_rejects_wrong_ids.stdout | 4 ++-- .../priroda/tests/ui/dap_scopes_variables.stdout | 8 ++++---- .../tests/ui/dap_scopes_variables_next.stdout | 12 ++++++------ .../miri/priroda/tests/ui/dap_stack_trace.stdout | 8 ++++---- src/tools/miri/priroda/tests/ui/dap_threads.stdout | 4 ++-- 12 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 2e76ccdbb2a47..838fd273b9a3c 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -19,7 +19,6 @@ use crate::debugger::{LocalDesc, PrirodaContext, StepResult}; const THREAD_ID: i64 = 1; const STACK_FRAME_ID: i64 = 1; const LOCALS_VARIABLES_REFERENCE: i64 = 1; -const MAX_REQUEST_COUNT: usize = 128; type ServerResult = Result; enum DispatchOutcome { @@ -51,7 +50,6 @@ impl Dap { &self, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx> { - // FIXME: make this unbounded once Priroda has a full session lifecycle. if let Err(err) = DapSession::stdio().run_requests(session)? { eprintln!("priroda dap error: {err:?}"); } @@ -83,7 +81,7 @@ impl DapSession { &mut self, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { - for _ in 0..MAX_REQUEST_COUNT { + loop { let request = match self.server.poll_request() { Ok(Some(request)) => request, Ok(None) => return interp_ok(Ok(())), @@ -96,8 +94,6 @@ impl DapSession { Err(err) => return interp_ok(Err(err)), } } - - interp_ok(Ok(())) } fn dispatch_request<'tcx>( @@ -290,7 +286,7 @@ impl DapSession { Source { name: path.file_name().map(|name| name.to_string_lossy().into_owned()), path: Some(path.display().to_string()), - source_reference: None, + source_reference: Some(0), presentation_hint: None, origin: None, sources: None, @@ -337,6 +333,7 @@ impl DapSession { let response = request.success(ResponseBody::Initialize(Capabilities { supports_configuration_done_request: Some(true), + supports_single_thread_execution_requests: Some(true), ..Capabilities::default() })); self.server.respond(response)?; diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout index 595a84f405b40..8727976fe724c 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout @@ -1,5 +1,5 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout index 1bf97320e659e..79d465699abeb 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout index af8fbfd94f4fa..606f03229a6ef 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout index 1e3a55ff64ef5..b07888d49086c 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 148 diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout index 69906b4578351..1b63dca0d5d28 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout index d81c109466e4c..1079ba07ca74c 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout index 0230fe0e3e1c4..cd19d314bcfe3 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout index d7a47ffb2d178..5d21127c1b6ee 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 @@ -8,9 +8,9 @@ Content-Length: 143 {"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 304 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 324 -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs"},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 {"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout index 3620bd1a0b9cb..58dc5937016fc 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 @@ -8,9 +8,9 @@ Content-Length: 143 {"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 314 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 334 -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs"},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 {"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 @@ -18,9 +18,9 @@ Content-Length: 143 {"seq":9,"type":"response","request_seq":7,"success":true,"command":"next","error":null}Content-Length: 186 -{"seq":10,"type":"event","event":"stopped","body":{"reason":"step","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 315 +{"seq":10,"type":"event","event":"stopped","body":{"reason":"step","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 335 -{"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs"},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: 219 +{"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: 219 {"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 528 diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout index 318eefe286bac..4d2c505f23aec 100644 --- a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 @@ -10,6 +10,6 @@ Content-Length: 143 {"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 295 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 315 -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_stack_trace.rs","path":"{MANIFEST_DIR}/tests/ui/dap_stack_trace.rs"},"line":3,"column":11}],"totalFrames":1},"error":null} \ No newline at end of file +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_stack_trace.rs","path":"{MANIFEST_DIR}/tests/ui/dap_stack_trace.rs","sourceReference":0},"line":3,"column":11}],"totalFrames":1},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.stdout b/src/tools/miri/priroda/tests/ui/dap_threads.stdout index 953d114e22ec2..d9b5878b17adf 100644 --- a/src/tools/miri/priroda/tests/ui/dap_threads.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdout @@ -1,6 +1,6 @@ -Content-Length: 143 +Content-Length: 188 -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 {"seq":2,"type":"event","event":"initialized"}Content-Length: 90 From adb5bb2502d75ee61139813eaaf0a207cd592056 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Mon, 3 Aug 2026 16:15:59 +0300 Subject: [PATCH 30/42] [Priroda] Normalise DAP Content-Length in test output DAP Content-Length headers embed the byte count of the following JSON, which drifts after path normalisation replaces the real manifest dir with {MANIFEST_DIR}. Replace Content-Length values with a {CONTENT_LENGTH} placeholder so path-length differences between machines do not cause spurious Content-Length mismatches in CI. --- src/tools/miri/priroda/tests/cli.rs | 6 ++++ .../priroda/tests/ui/dap_initialize.stdout | 4 +-- .../tests/ui/dap_initialize_launch.stdout | 6 ++-- ...nitialize_launch_configuration_done.stdout | 10 +++---- ...ts_configuration_done_before_launch.stdout | 10 +++---- ...ects_next_before_configuration_done.stdout | 12 ++++---- .../dap_rejects_non_initialize_first.stdout | 2 +- ...rejects_repeated_configuration_done.stdout | 16 +++++----- .../tests/ui/dap_rejects_wrong_ids.stdout | 24 +++++++-------- .../tests/ui/dap_scopes_variables.stdout | 16 +++++----- .../tests/ui/dap_scopes_variables_next.stdout | 30 +++++++++---------- .../priroda/tests/ui/dap_stack_trace.stdout | 14 ++++----- .../miri/priroda/tests/ui/dap_threads.stdout | 12 ++++---- 13 files changed, 84 insertions(+), 78 deletions(-) diff --git a/src/tools/miri/priroda/tests/cli.rs b/src/tools/miri/priroda/tests/cli.rs index ff2ce7716348a..2bf7f22bd1d98 100644 --- a/src/tools/miri/priroda/tests/cli.rs +++ b/src/tools/miri/priroda/tests/cli.rs @@ -34,6 +34,11 @@ fn main() -> Result<(), Box> { let rustc_sysroot_regex = Regex::new(®ex::escape(&rustc_sysroot)).unwrap(); let pointer_regex = Regex::new(r"0x[0-9a-f]+\[alloc[0-9]+\]<[0-9]+>").unwrap(); let crlf_regex = Regex::new(r"\r\n").unwrap(); + // DAP Content-Length headers embed the byte count of the following JSON, + // which changes when path normalisation alters the embedded file paths. + // Replace them with a placeholder so path-length differences between + // machines do not make Content-Length drift from the normalised body. + let content_length_regex = Regex::new(r"Content-Length: \d+").unwrap(); config.comment_defaults.base().normalize_stdout.extend([ (manifest_dir_regex.into(), b"{MANIFEST_DIR}".to_vec()), (miri_dir_regex.into(), b"{MIRI_DIR}".to_vec()), @@ -41,6 +46,7 @@ fn main() -> Result<(), Box> { (pointer_regex.into(), b"{ALLOC_PTR}".to_vec()), // DAP frames use CRLF headers; keep checked-in stdout fixtures readable. (crlf_regex.into(), b"\n".to_vec()), + (content_length_regex.into(), b"Content-Length: {CONTENT_LENGTH}".to_vec()), ]); // Priroda CLI tests do not currently require annotation comments in the test files diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout index 8727976fe724c..4f6f29a60dbd7 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout @@ -1,5 +1,5 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":2,"type":"event","event":"initialized"} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout index 79d465699abeb..7ba36709bd123 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout @@ -1,7 +1,7 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} {"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout index 606f03229a6ef..121232f9aa271 100644 --- a/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout @@ -1,11 +1,11 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} {"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout index b07888d49086c..4a4df53ea5889 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout @@ -1,11 +1,11 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 148 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":false,"message":"configurationDone requires launch","command":"configurationDone","error":null}Content-Length: 94 +{"seq":3,"type":"response","request_seq":2,"success":false,"message":"configurationDone requires launch","command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"disconnect","error":null}Content-Length: 57 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} {"seq":5,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout index 1b63dca0d5d28..796935374a8eb 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout @@ -1,13 +1,13 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 134 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":false,"message":"request requires a stopped frame","command":"next","error":null}Content-Length: 94 +{"seq":4,"type":"response","request_seq":3,"success":false,"message":"request requires a stopped frame","command":"next","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"response","request_seq":4,"success":true,"command":"disconnect","error":null}Content-Length: 57 +{"seq":5,"type":"response","request_seq":4,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} {"seq":6,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout index 55ba4e862250f..7ad4e38819f8f 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout @@ -1,3 +1,3 @@ -Content-Length: 131 +Content-Length: {CONTENT_LENGTH} {"seq":1,"type":"response","request_seq":2,"success":false,"message":"initialize must be sent first","command":"next","error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout index 1079ba07ca74c..c7d7b63bf5608 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout @@ -1,17 +1,17 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 154 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":false,"message":"configurationDone may only be sent once","command":"configurationDone","error":null}Content-Length: 94 +{"seq":6,"type":"response","request_seq":4,"success":false,"message":"configurationDone may only be sent once","command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"disconnect","error":null}Content-Length: 57 +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} {"seq":8,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout index cd19d314bcfe3..6baf6351f6a7b 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout @@ -1,25 +1,25 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 101 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":false,"message":"unknown threadId","error":null}Content-Length: 100 +{"seq":6,"type":"response","request_seq":4,"success":false,"message":"unknown threadId","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":false,"message":"unknown frameId","error":null}Content-Length: 111 +{"seq":7,"type":"response","request_seq":5,"success":false,"message":"unknown frameId","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":8,"type":"response","request_seq":6,"success":false,"message":"unknown variablesReference","error":null}Content-Length: 118 +{"seq":8,"type":"response","request_seq":6,"success":false,"message":"unknown variablesReference","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":9,"type":"response","request_seq":7,"success":false,"message":"unknown threadId","command":"next","error":null}Content-Length: 121 +{"seq":9,"type":"response","request_seq":7,"success":false,"message":"unknown threadId","command":"next","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":10,"type":"response","request_seq":8,"success":false,"message":"unknown threadId","command":"stepIn","error":null}Content-Length: 95 +{"seq":10,"type":"response","request_seq":8,"success":false,"message":"unknown threadId","command":"stepIn","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":11,"type":"response","request_seq":9,"success":true,"command":"disconnect","error":null}Content-Length: 58 +{"seq":11,"type":"response","request_seq":9,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} {"seq":12,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout index 5d21127c1b6ee..70c0765611115 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout @@ -1,17 +1,17 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 324 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout index 58dc5937016fc..68e2e00bd74db 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout @@ -1,31 +1,31 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 334 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: 218 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 527 +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: 88 +{"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":9,"type":"response","request_seq":7,"success":true,"command":"next","error":null}Content-Length: 186 +{"seq":9,"type":"response","request_seq":7,"success":true,"command":"next","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":10,"type":"event","event":"stopped","body":{"reason":"step","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 335 +{"seq":10,"type":"event","event":"stopped","body":{"reason":"step","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: 219 +{"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: 528 +{"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":13,"type":"response","request_seq":10,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"1_i32","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: 96 +{"seq":13,"type":"response","request_seq":10,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"1_i32","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":14,"type":"response","request_seq":11,"success":true,"command":"disconnect","error":null}Content-Length: 58 +{"seq":14,"type":"response","request_seq":11,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} {"seq":15,"type":"event","event":"terminated","body":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout index 4d2c505f23aec..1056d39e468b1 100644 --- a/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -1,15 +1,15 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: 315 +{"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":7,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_stack_trace.rs","path":"{MANIFEST_DIR}/tests/ui/dap_stack_trace.rs","sourceReference":0},"line":3,"column":11}],"totalFrames":1},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_threads.stdout b/src/tools/miri/priroda/tests/ui/dap_threads.stdout index d9b5878b17adf..56702d4adc22e 100644 --- a/src/tools/miri/priroda/tests/ui/dap_threads.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdout @@ -1,13 +1,13 @@ -Content-Length: 188 +Content-Length: {CONTENT_LENGTH} -{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: 46 +{"seq":1,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsSingleThreadExecutionRequests":true},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":2,"type":"event","event":"initialized"}Content-Length: 90 +{"seq":2,"type":"event","event":"initialized"}Content-Length: {CONTENT_LENGTH} -{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: 101 +{"seq":3,"type":"response","request_seq":2,"success":true,"command":"launch","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: 186 +{"seq":4,"type":"response","request_seq":3,"success":true,"command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} -{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: 135 +{"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} {"seq":6,"type":"response","request_seq":4,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null} \ No newline at end of file From a0c286ec076009516e24745824e878f70fb2a6e7 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 19:19:32 +0300 Subject: [PATCH 31/42] [Priroda] render interpreter errors via InterpError::to_string --- src/tools/miri/priroda/src/debugger.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs index aca596dc3f18e..b2b7c8779709a 100644 --- a/src/tools/miri/priroda/src/debugger.rs +++ b/src/tools/miri/priroda/src/debugger.rs @@ -3,7 +3,7 @@ use std::ops::Range; use std::path::PathBuf; use miri::Immediate::Uninit; -use miri::{interpret, *}; +use miri::*; use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; use rustc_hir::def::CtorKind; use rustc_middle::mir::interpret::AllocId; @@ -640,7 +640,7 @@ impl<'tcx> PrirodaContext<'tcx> { Either::Left(mplace) => match self.render_mplace_bytes(&mplace).report_err() { Ok(bytes) => bytes, - Err(err) => format!("", interpret::format_interp_error(err)), + Err(err) => format!("", err.to_string()), }, } } @@ -809,9 +809,7 @@ impl<'tcx> PrirodaContext<'tcx> { .ecx .eval_place_to_op(*place, None) .map(|op| self.render_source_shaped_op(op)) - .unwrap_or_else(|err| { - format!("", interpret::format_interp_error(err)) - }); + .unwrap_or_else(|err| format!("", err.to_string())); local_descs.push(LocalDesc { source_name: Some(var_debug_info.name), From 195fd937366a09dfa4edd752183f24eb46574eba Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 19:52:39 +0300 Subject: [PATCH 32/42] [Priroda] exhaustively list every DAP Command in dispatch and display List every Command variant in dispatch_request and display_command instead of a `_ =>` catch-all. New variants added upstream then fail to compile here instead of silently falling through the unsupported arm. --- src/tools/miri/priroda/src/frontend/dap.rs | 64 +++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 838fd273b9a3c..a55f4a517a086 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -149,7 +149,36 @@ impl DapSession { } Command::Disconnect(_) => interp_ok(self.handle_disconnect(request).map(|()| DispatchOutcome::Exit)), - _ => + Command::Attach(_) + | Command::BreakpointLocations(_) + | Command::Cancel(_) + | Command::Completions(_) + | Command::DataBreakpointInfo(_) + | Command::Disassemble(_) + | Command::Evaluate(_) + | Command::ExceptionInfo(_) + | Command::Goto(_) + | Command::GotoTargets(_) + | Command::LoadedSources + | Command::Modules(_) + | Command::Pause(_) + | Command::ReadMemory(_) + | Command::Restart(_) + | Command::RestartFrame(_) + | Command::ReverseContinue(_) + | Command::SetDataBreakpoints(_) + | Command::SetExceptionBreakpoints(_) + | Command::SetExpression(_) + | Command::SetFunctionBreakpoints(_) + | Command::SetInstructionBreakpoints(_) + | Command::SetVariable(_) + | Command::Source(_) + | Command::StepBack(_) + | Command::StepInTargets(_) + | Command::StepOut(_) + | Command::Terminate(_) + | Command::TerminateThreads(_) + | Command::WriteMemory(_) => interp_ok( self.handle_unsupported_request(request).map(|()| DispatchOutcome::Continue), ), @@ -636,7 +665,38 @@ impl DapSession { Command::Next(_) => "next", Command::StepIn(_) => "stepIn", Command::Disconnect(_) => "disconnect", - _ => "unsupported", + Command::Attach(_) => "attach", + Command::BreakpointLocations(_) => "breakpointLocations", + Command::Cancel(_) => "cancel", + Command::Completions(_) => "completions", + Command::Continue(_) => "continue", + Command::DataBreakpointInfo(_) => "dataBreakpointInfo", + Command::Disassemble(_) => "disassemble", + Command::Evaluate(_) => "evaluate", + Command::ExceptionInfo(_) => "exceptionInfo", + Command::Goto(_) => "goto", + Command::GotoTargets(_) => "gotoTargets", + Command::LoadedSources => "loadedSources", + Command::Modules(_) => "modules", + Command::Pause(_) => "pause", + Command::ReadMemory(_) => "readMemory", + Command::Restart(_) => "restart", + Command::RestartFrame(_) => "restartFrame", + Command::ReverseContinue(_) => "reverseContinue", + Command::SetBreakpoints(_) => "setBreakpoints", + Command::SetDataBreakpoints(_) => "setDataBreakpoints", + Command::SetExceptionBreakpoints(_) => "setExceptionBreakpoints", + Command::SetExpression(_) => "setExpression", + Command::SetFunctionBreakpoints(_) => "setFunctionBreakpoints", + Command::SetInstructionBreakpoints(_) => "setInstructionBreakpoints", + Command::SetVariable(_) => "setVariable", + Command::Source(_) => "source", + Command::StepBack(_) => "stepBack", + Command::StepInTargets(_) => "stepInTargets", + Command::StepOut(_) => "stepOut", + Command::Terminate(_) => "terminate", + Command::TerminateThreads(_) => "terminateThreads", + Command::WriteMemory(_) => "writeMemory", } } From e0bf0c70255dae5ab33ceb0f6dd7787de18c683b Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 19:54:02 +0300 Subject: [PATCH 33/42] [Priroda] route DAP request arguments into handlers, add setBreakpoints path guard Pull the inner arguments out of request.command at dispatch time and pass them by value into handle_scopes / handle_variables / handle_set_breakpoints, so the handlers no longer re-match on request.command. require_frame_id and require_variables_reference take the extracted value; the bug! fallback for the dispatch-only command in handle_variables is gone. Replace the inline DapState::Fresh check in dispatch_request with a require_initialized predicate, mirroring the other require_* guards. Reject setBreakpoints with an error when source.path is missing -- Priroda only resolves file-based breakpoints. --- src/tools/miri/priroda/src/frontend/dap.rs | 129 ++++++++++++--------- 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index a55f4a517a086..63adf6ed9ddcc 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,6 +1,7 @@ use std::io::{self, BufReader, BufWriter}; use emmy_dap_types::prelude::events::{ExitedEventBody, StoppedEventBody}; +use emmy_dap_types::prelude::requests::SetBreakpointsArguments; use emmy_dap_types::prelude::responses::{ ContinueResponse, ScopesResponse, SetBreakpointsResponse, StackTraceResponse, ThreadsResponse, VariablesResponse, @@ -101,11 +102,12 @@ impl DapSession { request: Request, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, ServerResult> { - if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { - return interp_ok( - self.respond_error(request, "initialize must be sent first") - .map(|()| DispatchOutcome::Continue), - ); + let uninitialized = match self.require_initialized(&request) { + Ok(rejected) => rejected, + Err(err) => return interp_ok(Err(err)), + }; + if uninitialized { + return interp_ok(Ok(DispatchOutcome::Continue)); } match &request.command { @@ -123,21 +125,31 @@ impl DapSession { interp_ok( self.handle_stack_trace(request, session).map(|()| DispatchOutcome::Continue), ), - Command::Scopes(_) => - interp_ok(self.handle_scopes(request, session).map(|()| DispatchOutcome::Continue)), - Command::Variables(_) => + Command::Scopes(args) => { + let frame_id = args.frame_id; interp_ok( - self.handle_variables(request, session).map(|()| DispatchOutcome::Continue), - ), + self.handle_scopes(request, frame_id, session) + .map(|()| DispatchOutcome::Continue), + ) + } + Command::Variables(args) => { + let variables_reference = args.variables_reference; + interp_ok( + self.handle_variables(request, variables_reference, session) + .map(|()| DispatchOutcome::Continue), + ) + } Command::Continue(_) => { let res = self.handle_continue(request, session)?; interp_ok(res.map(|()| self.dispatch_outcome())) } - Command::SetBreakpoints(_) => + Command::SetBreakpoints(args) => { + let args = args.clone(); interp_ok( - self.handle_set_breakpoints(request, session) + self.handle_set_breakpoints(request, &args, session) .map(|()| DispatchOutcome::Continue), - ), + ) + } Command::Next(_) | Command::StepIn(_) => { let body = match &request.command { Command::Next(_) => ResponseBody::Next, @@ -202,11 +214,12 @@ impl DapSession { fn handle_scopes<'tcx>( &mut self, request: Request, - _session: &PrirodaContext<'tcx>, + frame_id: i64, + session: &PrirodaContext<'tcx>, ) -> ServerResult { if self.reject_after_termination(&request)? || self.require_stopped(&request)? - || self.require_frame_id(&request)? + || self.require_frame_id(&request, frame_id)? { return Ok(()); } @@ -232,19 +245,20 @@ impl DapSession { fn handle_variables<'tcx>( &mut self, request: Request, + variables_reference: i64, session: &PrirodaContext<'tcx>, ) -> ServerResult { if self.reject_after_termination(&request)? || self.require_stopped(&request)? - || self.require_variables_reference(&request)? + || self.require_variables_reference(&request, variables_reference)? { return Ok(()); } - let variables = match &request.command { - Command::Variables(_) => - session.list_locals().into_iter().map(Self::local_to_variable).collect(), - _ => bug!("dispatch routes only Variables to handle_variables"), + let variables = if variables_reference == LOCALS_VARIABLES_REFERENCE { + session.list_locals().into_iter().map(Self::local_to_variable).collect() + } else { + Vec::new() }; let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); @@ -430,34 +444,38 @@ impl DapSession { fn handle_set_breakpoints<'tcx>( &mut self, request: Request, + args: &SetBreakpointsArguments, session: &mut PrirodaContext<'tcx>, ) -> ServerResult { if self.reject_after_termination(&request)? { return Ok(()); } + let Some(ref path_str) = args.source.path else { + return self.respond_error( + request, + "setBreakpoints requires a source.path; sourceReference loads are not supported", + ); + }; + + let path = std::path::PathBuf::from(path_str); let mut breakpoints = Vec::new(); - if let Command::SetBreakpoints(ref args) = request.command { - if let Some(ref path_str) = args.source.path { - let path = std::path::PathBuf::from(path_str); - if let Some(ref req_bps) = args.breakpoints { - for req_bp in req_bps { - let line = req_bp.line as usize; - session.set_breakpoint(path.clone(), line); - breakpoints.push(DapBreakpoint { - verified: true, - message: None, - source: Some(args.source.clone()), - line: Some(req_bp.line), - column: req_bp.column, - end_line: None, - end_column: None, - id: None, - instruction_reference: None, - offset: None, - }); - } - } + if let Some(ref req_bps) = args.breakpoints { + for req_bp in req_bps { + let line = req_bp.line as usize; + session.set_breakpoint(path.clone(), line); + breakpoints.push(DapBreakpoint { + verified: true, + message: None, + source: Some(args.source.clone()), + line: Some(req_bp.line), + column: req_bp.column, + end_line: None, + end_column: None, + id: None, + instruction_reference: None, + offset: None, + }); } } @@ -504,6 +522,15 @@ impl DapSession { Ok(false) } + fn require_initialized(&mut self, request: &Request) -> ServerResult { + if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { + self.server.respond(request.clone().error("initialize must be sent first"))?; + return Ok(true); + } + + Ok(false) + } + fn require_stopped(&mut self, request: &Request) -> ServerResult { if self.state != DapState::Stopped { self.server.respond(request.clone().error("request requires a stopped frame"))?; @@ -530,12 +557,8 @@ impl DapSession { Ok(false) } - fn require_frame_id(&mut self, request: &Request) -> ServerResult { - let Command::Scopes(args) = &request.command else { - bug!("dispatch routes only scopes to require_frame_id"); - }; - - if args.frame_id != STACK_FRAME_ID { + fn require_frame_id(&mut self, request: &Request, frame_id: i64) -> ServerResult { + if frame_id != STACK_FRAME_ID { self.server.respond(request.clone().error("unknown frameId"))?; return Ok(true); } @@ -543,12 +566,12 @@ impl DapSession { Ok(false) } - fn require_variables_reference(&mut self, request: &Request) -> ServerResult { - let Command::Variables(args) = &request.command else { - bug!("dispatch routes only variables to require_variables_reference"); - }; - - if args.variables_reference != LOCALS_VARIABLES_REFERENCE { + fn require_variables_reference( + &mut self, + request: &Request, + variables_reference: i64, + ) -> ServerResult { + if variables_reference != LOCALS_VARIABLES_REFERENCE { self.server.respond(request.clone().error("unknown variablesReference"))?; return Ok(true); } From b723b1276d90b409e5dd5cf4c591aff740ec4f10 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 19:54:15 +0300 Subject: [PATCH 34/42] [Priroda] fill DAP Locals scope source position from current frame The Locals scope carried no source/line/column, so the editor could not anchor the variables view to the stopped frame. Pull them from session.current_location when present and bless the dap_scopes_variables* fixtures to the new fields. --- src/tools/miri/priroda/src/frontend/dap.rs | 29 +++++++++++++++++-- .../tests/ui/dap_scopes_variables.stdout | 2 +- .../tests/ui/dap_scopes_variables_next.stdout | 4 +-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 63adf6ed9ddcc..ec2f26e699f17 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -224,6 +224,29 @@ impl DapSession { return Ok(()); } + let (source, line, column) = match &session.current_location { + Some(location) => { + let source = session.local_path(location).as_ref().map(|path| { + Source { + name: path.file_name().map(|name| name.to_string_lossy().into_owned()), + path: Some(path.display().to_string()), + source_reference: Some(0), + presentation_hint: None, + origin: None, + sources: None, + checksums: None, + } + }); + let line = + location.line.try_into().unwrap_or_else(|_| bug!("source line exceeds i64")); + let column = location + .column + .try_into() + .unwrap_or_else(|_| bug!("source column exceeds i64")); + (source, Some(line), Some(column)) + } + None => (None, None, None), + }; let response = request.success(ResponseBody::Scopes(ScopesResponse { scopes: vec![Scope { name: "Locals".to_string(), @@ -232,9 +255,9 @@ impl DapSession { named_variables: None, indexed_variables: Some(0), expensive: false, - source: None, - line: None, - column: None, + source, + line, + column, end_line: None, end_column: None, }], diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout index 70c0765611115..4cc848bc88369 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout @@ -12,6 +12,6 @@ Content-Length: {CONTENT_LENGTH} {"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false,"source":{"name":"dap_scopes_variables.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables.rs","sourceReference":0},"line":4,"column":9}]},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null} \ No newline at end of file diff --git a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout index 68e2e00bd74db..558af9b383840 100644 --- a/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout @@ -12,7 +12,7 @@ Content-Length: {CONTENT_LENGTH} {"seq":6,"type":"response","request_seq":4,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":4,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} +{"seq":7,"type":"response","request_seq":5,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false,"source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":4,"column":9}]},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":8,"type":"response","request_seq":6,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: {CONTENT_LENGTH} @@ -22,7 +22,7 @@ Content-Length: {CONTENT_LENGTH} {"seq":11,"type":"response","request_seq":8,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1,"name":"main","source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":5,"column":9}],"totalFrames":1},"error":null}Content-Length: {CONTENT_LENGTH} -{"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false}]},"error":null}Content-Length: {CONTENT_LENGTH} +{"seq":12,"type":"response","request_seq":9,"success":true,"command":"scopes","body":{"scopes":[{"name":"Locals","presentationHint":"locals","variablesReference":1,"indexedVariables":0,"expensive":false,"source":{"name":"dap_scopes_variables_next.rs","path":"{MANIFEST_DIR}/tests/ui/dap_scopes_variables_next.rs","sourceReference":0},"line":5,"column":9}]},"error":null}Content-Length: {CONTENT_LENGTH} {"seq":13,"type":"response","request_seq":10,"success":true,"command":"variables","body":{"variables":[{"name":"_0","value":"","type":"()","variablesReference":0},{"name":"x","value":"1_i32","type":"i32","variablesReference":0},{"name":"y","value":"","type":"bool","variablesReference":0},{"name":"_3","value":"","type":"(i32, bool)","variablesReference":0},{"name":"_4","value":"","type":"i32","variablesReference":0},{"name":"_5","value":"","type":"bool","variablesReference":0}]},"error":null}Content-Length: {CONTENT_LENGTH} From 66470f594ef414400f68f4dde6419ffdefada9f6 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 18:03:13 +0300 Subject: [PATCH 35/42] [Priroda] rework DAP dispatch return type for bubble-up rejections Convert every require_* and reject_after_termination predicate from ServerResult eager-respond to pure Result<(), &str>, add DispatchOutcome::Rejected(&str), and change handlers to return Result. Once predicates stop eagerly responding, Rejected carries their errors out -- and vice versa. dispatch_request return type becomes InterpResult>. run_requests clones the request before dispatch so the original stays available for request.error(msg) when a Rejected bubbles up. Handlers rebuilt to if let Err(msg) = ...{ return Ok(Rejected(msg)); } + Ok(DispatchOutcome::Continue) endings; and_then chains in the execution handlers map to DispatchOutcome::Continue, and respond_error is gone from the happy path. --- src/tools/miri/priroda/src/frontend/dap.rs | 381 ++++++++++----------- 1 file changed, 173 insertions(+), 208 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index ec2f26e699f17..847674a7cfae8 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -1,5 +1,6 @@ use std::io::{self, BufReader, BufWriter}; +use emmy_dap_types::errors::ServerError; use emmy_dap_types::prelude::events::{ExitedEventBody, StoppedEventBody}; use emmy_dap_types::prelude::requests::SetBreakpointsArguments; use emmy_dap_types::prelude::responses::{ @@ -25,6 +26,7 @@ type ServerResult = Result; enum DispatchOutcome { Continue, Exit, + Rejected(&'static str), } #[derive(Clone, Copy, PartialEq, Eq)] @@ -89,9 +91,17 @@ impl DapSession { Err(err) => return interp_ok(Err(err)), }; - match self.dispatch_request(request, session)? { + let request_for_dispatch = request.clone(); + + match self.dispatch_request(request_for_dispatch, session)? { Ok(DispatchOutcome::Continue) => {} Ok(DispatchOutcome::Exit) => return interp_ok(Ok(())), + Ok(DispatchOutcome::Rejected(msg)) => { + let response = request.error(msg); + if let Err(err) = self.server.respond(response) { + return interp_ok(Err(err)); + } + } Err(err) => return interp_ok(Err(err)), } } @@ -101,54 +111,29 @@ impl DapSession { &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, ServerResult> { - let uninitialized = match self.require_initialized(&request) { - Ok(rejected) => rejected, - Err(err) => return interp_ok(Err(err)), - }; - if uninitialized { - return interp_ok(Ok(DispatchOutcome::Continue)); - } - - match &request.command { - Command::Initialize(_) => - interp_ok(self.handle_initialize(request).map(|()| DispatchOutcome::Continue)), - Command::Launch(_) => - interp_ok(self.handle_launch(request).map(|()| DispatchOutcome::Continue)), - Command::ConfigurationDone => { - let res = self.handle_configuration_done(request, session)?; - interp_ok(res.map(|()| self.dispatch_outcome())) - } - Command::Threads => - interp_ok(self.handle_threads(request).map(|()| DispatchOutcome::Continue)), - Command::StackTrace(_) => - interp_ok( - self.handle_stack_trace(request, session).map(|()| DispatchOutcome::Continue), - ), + ) -> InterpResult<'tcx, Result> { + if let Err(msg) = self.require_initialized(&request) { + return interp_ok(Ok(DispatchOutcome::Rejected(msg))); + } + + let outcome = match &request.command { + Command::Initialize(_) => self.handle_initialize(request), + Command::Launch(_) => self.handle_launch(request), + Command::ConfigurationDone => return self.handle_configuration_done(request, session), + Command::Threads => self.handle_threads(request), + Command::StackTrace(_) => self.handle_stack_trace(request, session), Command::Scopes(args) => { let frame_id = args.frame_id; - interp_ok( - self.handle_scopes(request, frame_id, session) - .map(|()| DispatchOutcome::Continue), - ) + self.handle_scopes(request, frame_id, session) } Command::Variables(args) => { let variables_reference = args.variables_reference; - interp_ok( - self.handle_variables(request, variables_reference, session) - .map(|()| DispatchOutcome::Continue), - ) - } - Command::Continue(_) => { - let res = self.handle_continue(request, session)?; - interp_ok(res.map(|()| self.dispatch_outcome())) + self.handle_variables(request, variables_reference, session) } + Command::Continue(_) => return self.handle_continue(request, session), Command::SetBreakpoints(args) => { let args = args.clone(); - interp_ok( - self.handle_set_breakpoints(request, &args, session) - .map(|()| DispatchOutcome::Continue), - ) + self.handle_set_breakpoints(request, &args, session) } Command::Next(_) | Command::StepIn(_) => { let body = match &request.command { @@ -156,11 +141,9 @@ impl DapSession { Command::StepIn(_) => ResponseBody::StepIn, _ => bug!("step body is selected by the outer Next/StepIn match"), }; - let res = self.handle_step(request, body, session)?; - interp_ok(res.map(|()| self.dispatch_outcome())) + return self.handle_step(request, body, session); } - Command::Disconnect(_) => - interp_ok(self.handle_disconnect(request).map(|()| DispatchOutcome::Exit)), + Command::Disconnect(_) => self.handle_disconnect(request), Command::Attach(_) | Command::BreakpointLocations(_) | Command::Cancel(_) @@ -190,25 +173,21 @@ impl DapSession { | Command::StepOut(_) | Command::Terminate(_) | Command::TerminateThreads(_) - | Command::WriteMemory(_) => - interp_ok( - self.handle_unsupported_request(request).map(|()| DispatchOutcome::Continue), - ), - } + | Command::WriteMemory(_) => self.handle_unsupported_request(request), + }; + interp_ok(outcome) } /// FIXME: connect launch arguments to Priroda's session model. - fn handle_launch(&mut self, request: Request) -> ServerResult { - if self.reject_after_termination(&request)? - || self.require_state(&request, DapState::Initialized, "launch requires initialize")? - { - return Ok(()); + fn handle_launch(&mut self, request: Request) -> Result { + if let Err(msg) = self.require_state(DapState::Initialized) { + return Ok(DispatchOutcome::Rejected(msg)); } let response = request.success(ResponseBody::Launch); self.server.respond(response)?; self.state = DapState::Launched; - Ok(()) + Ok(DispatchOutcome::Continue) } fn handle_scopes<'tcx>( @@ -216,12 +195,12 @@ impl DapSession { request: Request, frame_id: i64, session: &PrirodaContext<'tcx>, - ) -> ServerResult { - if self.reject_after_termination(&request)? - || self.require_stopped(&request)? - || self.require_frame_id(&request, frame_id)? - { - return Ok(()); + ) -> Result { + if let Err(msg) = self.require_stopped() { + return Ok(DispatchOutcome::Rejected(msg)); + } + if let Err(msg) = Self::require_frame_id(frame_id) { + return Ok(DispatchOutcome::Rejected(msg)); } let (source, line, column) = match &session.current_location { @@ -262,7 +241,8 @@ impl DapSession { end_column: None, }], })); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } fn handle_variables<'tcx>( @@ -270,12 +250,12 @@ impl DapSession { request: Request, variables_reference: i64, session: &PrirodaContext<'tcx>, - ) -> ServerResult { - if self.reject_after_termination(&request)? - || self.require_stopped(&request)? - || self.require_variables_reference(&request, variables_reference)? - { - return Ok(()); + ) -> Result { + if let Err(msg) = self.require_stopped() { + return Ok(DispatchOutcome::Rejected(msg)); + } + if let Err(msg) = Self::require_variables_reference(variables_reference) { + return Ok(DispatchOutcome::Rejected(msg)); } let variables = if variables_reference == LOCALS_VARIABLES_REFERENCE { @@ -285,29 +265,33 @@ impl DapSession { }; let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } fn handle_configuration_done<'tcx>( &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, ServerResult> { - let rejected = match self.check_configuration_done_request(&request) { - Ok(rejected) => rejected, + ) -> InterpResult<'tcx, Result> { + match self.check_configuration_done_request() { + Ok(DispatchOutcome::Continue) => {} + Ok(other) => return interp_ok(Ok(other)), Err(err) => return interp_ok(Err(err)), - }; - if rejected { - return interp_ok(Ok(())); } match Self::execution_outcome(session.stop_at_first_user_location()) { ExecutionOutcome::Stopped(_) => { let response = request.success(ResponseBody::ConfigurationDone); - interp_ok(self.server.respond(response).and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(StoppedEventReason::Entry) - })) + interp_ok( + self.server + .respond(response) + .and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(StoppedEventReason::Entry) + }) + .map(|()| DispatchOutcome::Continue), + ) } ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, ResponseBody::ConfigurationDone, code)), @@ -318,15 +302,16 @@ impl DapSession { /// FIXME: replace this with Miri thread state once Priroda exposes a /// frontend-facing thread model. - fn handle_threads(&mut self, request: Request) -> ServerResult { - if self.reject_after_termination(&request)? { - return Ok(()); + fn handle_threads(&mut self, request: Request) -> Result { + if let Err(msg) = self.reject_after_termination() { + return Ok(DispatchOutcome::Rejected(msg)); } let response = request.success(ResponseBody::Threads(ThreadsResponse { threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], })); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } /// FIXME: report all frames once Priroda exposes a frontend-facing stack model. @@ -334,12 +319,12 @@ impl DapSession { &mut self, request: Request, session: &PrirodaContext<'tcx>, - ) -> ServerResult { - if self.reject_after_termination(&request)? - || self.require_stopped(&request)? - || self.require_thread_id(&request)? - { - return Ok(()); + ) -> Result { + if let Err(msg) = self.require_stopped() { + return Ok(DispatchOutcome::Rejected(msg)); + } + if let Err(msg) = Self::require_thread_id(&request) { + return Ok(DispatchOutcome::Rejected(msg)); } let stack_frames = match &session.current_location { @@ -383,18 +368,14 @@ impl DapSession { stack_frames, total_frames: Some(total_frames), })); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } /// FIXME: grow capabilities as Priroda adds DAP features. - fn handle_initialize(&mut self, request: Request) -> ServerResult { - // Advertise configurationDone support ahead of its handler so VS Code - // completes the full handshake; the handler arrives in a later commit. - if self.reject_after_termination(&request)? { - return Ok(()); - } + fn handle_initialize(&mut self, request: Request) -> Result { if self.state != DapState::Fresh { - return self.respond_error(request, "initialize may only be sent once"); + return Ok(DispatchOutcome::Rejected("initialize may only be sent once")); } let response = request.success(ResponseBody::Initialize(Capabilities { @@ -405,7 +386,7 @@ impl DapSession { self.server.respond(response)?; self.server.send_event(Event::Initialized)?; self.state = DapState::Initialized; - Ok(()) + Ok(DispatchOutcome::Continue) } /// FIXME: distinguish step-over from step-in once Priroda has call-aware stepping. @@ -414,21 +395,24 @@ impl DapSession { request: Request, body: ResponseBody, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, ServerResult> { - let rejected = match self.check_step_request(&request) { - Ok(rejected) => rejected, + ) -> InterpResult<'tcx, Result> { + match self.check_step_request(&request) { + Ok(DispatchOutcome::Continue) => {} + Ok(other) => return interp_ok(Ok(other)), Err(err) => return interp_ok(Err(err)), - }; - if rejected { - return interp_ok(Ok(())); } match Self::execution_outcome(session.step()) { ExecutionOutcome::Stopped(result) => - interp_ok(self.server.respond(request.success(body)).and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(Self::stopped_reason(result)) - })), + interp_ok( + self.server + .respond(request.success(body)) + .and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(Self::stopped_reason(result)) + }) + .map(|()| DispatchOutcome::Continue), + ), ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, body, code)), ExecutionOutcome::Failed(message) => @@ -440,23 +424,26 @@ impl DapSession { &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, ServerResult> { - let rejected = match self.check_step_request(&request) { - Ok(rejected) => rejected, + ) -> InterpResult<'tcx, Result> { + match self.check_step_request(&request) { + Ok(DispatchOutcome::Continue) => {} + Ok(other) => return interp_ok(Ok(other)), Err(err) => return interp_ok(Err(err)), - }; - if rejected { - return interp_ok(Ok(())); } let body = ResponseBody::Continue(ContinueResponse { all_threads_continued: Some(true) }); match Self::execution_outcome(session.continue_execution()) { ExecutionOutcome::Stopped(result) => - interp_ok(self.server.respond(request.success(body)).and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(Self::stopped_reason(result)) - })), + interp_ok( + self.server + .respond(request.success(body)) + .and_then(|()| { + self.state = DapState::Stopped; + self.send_stopped_event(Self::stopped_reason(result)) + }) + .map(|()| DispatchOutcome::Continue), + ), ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, body, code)), ExecutionOutcome::Failed(message) => @@ -469,16 +456,15 @@ impl DapSession { request: Request, args: &SetBreakpointsArguments, session: &mut PrirodaContext<'tcx>, - ) -> ServerResult { - if self.reject_after_termination(&request)? { - return Ok(()); + ) -> Result { + if let Err(msg) = self.reject_after_termination() { + return Ok(DispatchOutcome::Rejected(msg)); } let Some(ref path_str) = args.source.path else { - return self.respond_error( - request, + return Ok(DispatchOutcome::Rejected( "setBreakpoints requires a source.path; sourceReference loads are not supported", - ); + )); }; let path = std::path::PathBuf::from(path_str); @@ -504,66 +490,63 @@ impl DapSession { let response = request.success(ResponseBody::SetBreakpoints(SetBreakpointsResponse { breakpoints })); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } - fn handle_disconnect(&mut self, request: Request) -> ServerResult { + fn handle_disconnect(&mut self, request: Request) -> Result { self.server.respond(request.success(ResponseBody::Disconnect))?; self.state = DapState::Terminated; - self.server.send_event(Event::Terminated(None)) + self.server.send_event(Event::Terminated(None))?; + Ok(DispatchOutcome::Exit) } - fn handle_unsupported_request(&mut self, request: Request) -> ServerResult { + fn handle_unsupported_request( + &mut self, + request: Request, + ) -> Result { let message = format!( "unsupported request in Priroda DAP demo mode: {}", Self::display_command(&request.command) ); let response = request.error(&message); - self.server.respond(response) + self.server.respond(response)?; + Ok(DispatchOutcome::Continue) } - fn reject_after_termination(&mut self, request: &Request) -> ServerResult { + fn reject_after_termination(&self) -> Result<(), &'static str> { if self.state == DapState::Terminated { - self.server.respond(request.clone().error("request received after termination"))?; - return Ok(true); + return Err("request received after termination"); } - - Ok(false) + Ok(()) } - fn require_state( - &mut self, - request: &Request, - expected: DapState, - message: &'static str, - ) -> ServerResult { + fn require_state(&self, expected: DapState) -> Result<(), &'static str> { if self.state != expected { - self.server.respond(request.clone().error(message))?; - return Ok(true); + return Err(match expected { + DapState::Initialized => "launch requires initialize", + DapState::Launched => "configurationDone requires launch", + _ => "invalid session state for request", + }); } - - Ok(false) + Ok(()) } - fn require_initialized(&mut self, request: &Request) -> ServerResult { + fn require_initialized(&self, request: &Request) -> Result<(), &'static str> { if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { - self.server.respond(request.clone().error("initialize must be sent first"))?; - return Ok(true); + return Err("initialize must be sent first"); } - - Ok(false) + Ok(()) } - fn require_stopped(&mut self, request: &Request) -> ServerResult { + fn require_stopped(&self) -> Result<(), &'static str> { if self.state != DapState::Stopped { - self.server.respond(request.clone().error("request requires a stopped frame"))?; - return Ok(true); + return Err("request requires a stopped frame"); } - - Ok(false) + Ok(()) } - fn require_thread_id(&mut self, request: &Request) -> ServerResult { + fn require_thread_id(request: &Request) -> Result<(), &'static str> { let valid = match &request.command { Command::StackTrace(args) => args.thread_id == THREAD_ID, Command::Next(args) => args.thread_id == THREAD_ID, @@ -573,80 +556,62 @@ impl DapSession { }; if !valid { - self.server.respond(request.clone().error("unknown threadId"))?; - return Ok(true); + return Err("unknown threadId"); } - - Ok(false) + Ok(()) } - fn require_frame_id(&mut self, request: &Request, frame_id: i64) -> ServerResult { + fn require_frame_id(frame_id: i64) -> Result<(), &'static str> { if frame_id != STACK_FRAME_ID { - self.server.respond(request.clone().error("unknown frameId"))?; - return Ok(true); + return Err("unknown frameId"); } - - Ok(false) + Ok(()) } - fn require_variables_reference( - &mut self, - request: &Request, - variables_reference: i64, - ) -> ServerResult { + fn require_variables_reference(variables_reference: i64) -> Result<(), &'static str> { if variables_reference != LOCALS_VARIABLES_REFERENCE { - self.server.respond(request.clone().error("unknown variablesReference"))?; - return Ok(true); + return Err("unknown variablesReference"); } - - Ok(false) + Ok(()) } - fn check_configuration_done_request(&mut self, request: &Request) -> ServerResult { - if self.reject_after_termination(request)? { - return Ok(true); + fn check_configuration_done_request(&self) -> Result { + if let Err(msg) = self.reject_after_termination() { + return Ok(DispatchOutcome::Rejected(msg)); } - if self.state == DapState::Stopped { - self.server - .respond(request.clone().error("configurationDone may only be sent once"))?; - return Ok(true); + return Ok(DispatchOutcome::Rejected("configurationDone may only be sent once")); } - - if self.require_state(request, DapState::Launched, "configurationDone requires launch")? { - return Ok(true); + if let Err(msg) = self.require_state(DapState::Launched) { + return Ok(DispatchOutcome::Rejected(msg)); } - Ok(false) + Ok(DispatchOutcome::Continue) } - fn check_step_request(&mut self, request: &Request) -> ServerResult { - if self.reject_after_termination(request)? - || self.require_stopped(request)? - || self.require_thread_id(request)? - { - return Ok(true); + fn check_step_request(&self, request: &Request) -> Result { + if let Err(msg) = self.reject_after_termination() { + return Ok(DispatchOutcome::Rejected(msg)); } - - Ok(false) - } - - fn respond_error(&mut self, request: Request, message: &str) -> ServerResult { - self.server.respond(request.error(message)) - } - - fn dispatch_outcome(&self) -> DispatchOutcome { - if self.state == DapState::Terminated { - DispatchOutcome::Exit - } else { - DispatchOutcome::Continue + if let Err(msg) = self.require_stopped() { + return Ok(DispatchOutcome::Rejected(msg)); + } + if let Err(msg) = Self::require_thread_id(request) { + return Ok(DispatchOutcome::Rejected(msg)); } + + Ok(DispatchOutcome::Continue) } - fn respond_execution_error(&mut self, request: Request, message: String) -> ServerResult { + fn respond_execution_error( + &mut self, + request: Request, + message: String, + ) -> Result { self.state = DapState::Terminated; self.server.respond(request.error(&message))?; - self.server.send_event(Event::Terminated(None)) + self.server.send_event(Event::Terminated(None))?; + Ok(DispatchOutcome::Exit) } fn respond_terminated( @@ -654,12 +619,12 @@ impl DapSession { request: Request, body: ResponseBody, code: i32, - ) -> ServerResult { + ) -> Result { self.state = DapState::Terminated; self.server.respond(request.success(body))?; self.server.send_event(Event::Exited(ExitedEventBody { exit_code: code.into() }))?; self.server.send_event(Event::Terminated(None))?; - Ok(()) + Ok(DispatchOutcome::Exit) } fn execution_outcome<'tcx>(result: InterpResult<'tcx, StepResult>) -> ExecutionOutcome { From 223740293b6a8feb69239145e950dd8f3b3a8023 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 18:03:13 +0300 Subject: [PATCH 36/42] [Priroda] drop redundant termination guards in DAP handlers A bunch of reject_after_termination calls sat before a state check that already excludes Terminated, so the reject was dead. Dropped those. check_configuration_done_request and check_step_request collapse to their actual predicate -- require_state(Launched) on the first, require_stopped + require_thread_id on the second. The"configurationDone may only be sent once" arm is gone since require_state(Launched) already rejects Stopped. Updated dap_rejects_repeated_configuration_done.stdout to the new "configurationDone requires launch" message. --- src/tools/miri/priroda/src/frontend/dap.rs | 9 --------- .../ui/dap_rejects_repeated_configuration_done.stdout | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 847674a7cfae8..0098b6f8c7397 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -576,12 +576,6 @@ impl DapSession { } fn check_configuration_done_request(&self) -> Result { - if let Err(msg) = self.reject_after_termination() { - return Ok(DispatchOutcome::Rejected(msg)); - } - if self.state == DapState::Stopped { - return Ok(DispatchOutcome::Rejected("configurationDone may only be sent once")); - } if let Err(msg) = self.require_state(DapState::Launched) { return Ok(DispatchOutcome::Rejected(msg)); } @@ -590,9 +584,6 @@ impl DapSession { } fn check_step_request(&self, request: &Request) -> Result { - if let Err(msg) = self.reject_after_termination() { - return Ok(DispatchOutcome::Rejected(msg)); - } if let Err(msg) = self.require_stopped() { return Ok(DispatchOutcome::Rejected(msg)); } diff --git a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout index c7d7b63bf5608..abc6e1cf7d694 100644 --- a/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout @@ -10,7 +10,7 @@ Content-Length: {CONTENT_LENGTH} {"seq":5,"type":"event","event":"stopped","body":{"reason":"entry","description":null,"threadId":1,"preserveFocusHint":null,"text":null,"allThreadsStopped":true,"hitBreakpointIds":null}}Content-Length: {CONTENT_LENGTH} -{"seq":6,"type":"response","request_seq":4,"success":false,"message":"configurationDone may only be sent once","command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} +{"seq":6,"type":"response","request_seq":4,"success":false,"message":"configurationDone requires launch","command":"configurationDone","error":null}Content-Length: {CONTENT_LENGTH} {"seq":7,"type":"response","request_seq":5,"success":true,"command":"disconnect","error":null}Content-Length: {CONTENT_LENGTH} From 66e0a9eff7143ccf216483c9b0511a6915102286 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 18:06:12 +0300 Subject: [PATCH 37/42] [Priroda] bubble predicate failures through HandlerError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropped DispatchOutcome::Rejected in favor of HandlerError, which has Reject and Transport variants. Predicates stay Result<(), &str>; callers do .map_err(HandlerError::Reject)?. With From for HandlerError, self.server.respond(..)? in handlers just works. run_requests now sends request.error(msg) for rejections and bubbles transport errors out — one send per request. This addresses the feedback about Result and predicates eagerly reporting inside the require methods. --- src/tools/miri/priroda/src/frontend/dap.rs | 168 +++++++++------------ 1 file changed, 72 insertions(+), 96 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index 0098b6f8c7397..b110e90a4fb1e 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -23,10 +23,20 @@ const STACK_FRAME_ID: i64 = 1; const LOCALS_VARIABLES_REFERENCE: i64 = 1; type ServerResult = Result; -enum DispatchOutcome { +enum HandlerOutcome { Continue, Exit, - Rejected(&'static str), +} + +enum HandlerError { + Reject(&'static str), + Transport(ServerError), +} + +impl From for HandlerError { + fn from(e: ServerError) -> Self { + HandlerError::Transport(e) + } } #[derive(Clone, Copy, PartialEq, Eq)] @@ -94,15 +104,15 @@ impl DapSession { let request_for_dispatch = request.clone(); match self.dispatch_request(request_for_dispatch, session)? { - Ok(DispatchOutcome::Continue) => {} - Ok(DispatchOutcome::Exit) => return interp_ok(Ok(())), - Ok(DispatchOutcome::Rejected(msg)) => { + Ok(HandlerOutcome::Continue) => {} + Ok(HandlerOutcome::Exit) => return interp_ok(Ok(())), + Err(HandlerError::Reject(msg)) => { let response = request.error(msg); if let Err(err) = self.server.respond(response) { return interp_ok(Err(err)); } } - Err(err) => return interp_ok(Err(err)), + Err(HandlerError::Transport(e)) => return interp_ok(Err(e)), } } } @@ -111,9 +121,9 @@ impl DapSession { &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { + ) -> InterpResult<'tcx, Result> { if let Err(msg) = self.require_initialized(&request) { - return interp_ok(Ok(DispatchOutcome::Rejected(msg))); + return interp_ok(Err(HandlerError::Reject(msg))); } let outcome = match &request.command { @@ -179,15 +189,13 @@ impl DapSession { } /// FIXME: connect launch arguments to Priroda's session model. - fn handle_launch(&mut self, request: Request) -> Result { - if let Err(msg) = self.require_state(DapState::Initialized) { - return Ok(DispatchOutcome::Rejected(msg)); - } + fn handle_launch(&mut self, request: Request) -> Result { + self.require_state(DapState::Initialized).map_err(HandlerError::Reject)?; let response = request.success(ResponseBody::Launch); self.server.respond(response)?; self.state = DapState::Launched; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } fn handle_scopes<'tcx>( @@ -195,13 +203,9 @@ impl DapSession { request: Request, frame_id: i64, session: &PrirodaContext<'tcx>, - ) -> Result { - if let Err(msg) = self.require_stopped() { - return Ok(DispatchOutcome::Rejected(msg)); - } - if let Err(msg) = Self::require_frame_id(frame_id) { - return Ok(DispatchOutcome::Rejected(msg)); - } + ) -> Result { + self.require_stopped().map_err(HandlerError::Reject)?; + Self::require_frame_id(frame_id).map_err(HandlerError::Reject)?; let (source, line, column) = match &session.current_location { Some(location) => { @@ -242,7 +246,7 @@ impl DapSession { }], })); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } fn handle_variables<'tcx>( @@ -250,13 +254,9 @@ impl DapSession { request: Request, variables_reference: i64, session: &PrirodaContext<'tcx>, - ) -> Result { - if let Err(msg) = self.require_stopped() { - return Ok(DispatchOutcome::Rejected(msg)); - } - if let Err(msg) = Self::require_variables_reference(variables_reference) { - return Ok(DispatchOutcome::Rejected(msg)); - } + ) -> Result { + self.require_stopped().map_err(HandlerError::Reject)?; + Self::require_variables_reference(variables_reference).map_err(HandlerError::Reject)?; let variables = if variables_reference == LOCALS_VARIABLES_REFERENCE { session.list_locals().into_iter().map(Self::local_to_variable).collect() @@ -266,18 +266,16 @@ impl DapSession { let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } fn handle_configuration_done<'tcx>( &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - match self.check_configuration_done_request() { - Ok(DispatchOutcome::Continue) => {} - Ok(other) => return interp_ok(Ok(other)), - Err(err) => return interp_ok(Err(err)), + ) -> InterpResult<'tcx, Result> { + if let Err(msg) = self.require_state(DapState::Launched) { + return interp_ok(Err(HandlerError::Reject(msg))); } match Self::execution_outcome(session.stop_at_first_user_location()) { @@ -290,7 +288,8 @@ impl DapSession { self.state = DapState::Stopped; self.send_stopped_event(StoppedEventReason::Entry) }) - .map(|()| DispatchOutcome::Continue), + .map(|()| HandlerOutcome::Continue) + .map_err(HandlerError::Transport), ) } ExecutionOutcome::Terminated { code } => @@ -302,16 +301,14 @@ impl DapSession { /// FIXME: replace this with Miri thread state once Priroda exposes a /// frontend-facing thread model. - fn handle_threads(&mut self, request: Request) -> Result { - if let Err(msg) = self.reject_after_termination() { - return Ok(DispatchOutcome::Rejected(msg)); - } + fn handle_threads(&mut self, request: Request) -> Result { + self.reject_after_termination().map_err(HandlerError::Reject)?; let response = request.success(ResponseBody::Threads(ThreadsResponse { threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], })); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } /// FIXME: report all frames once Priroda exposes a frontend-facing stack model. @@ -319,13 +316,9 @@ impl DapSession { &mut self, request: Request, session: &PrirodaContext<'tcx>, - ) -> Result { - if let Err(msg) = self.require_stopped() { - return Ok(DispatchOutcome::Rejected(msg)); - } - if let Err(msg) = Self::require_thread_id(&request) { - return Ok(DispatchOutcome::Rejected(msg)); - } + ) -> Result { + self.require_stopped().map_err(HandlerError::Reject)?; + Self::require_thread_id(&request).map_err(HandlerError::Reject)?; let stack_frames = match &session.current_location { Some(location) => { @@ -369,13 +362,13 @@ impl DapSession { total_frames: Some(total_frames), })); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } /// FIXME: grow capabilities as Priroda adds DAP features. - fn handle_initialize(&mut self, request: Request) -> Result { + fn handle_initialize(&mut self, request: Request) -> Result { if self.state != DapState::Fresh { - return Ok(DispatchOutcome::Rejected("initialize may only be sent once")); + return Err(HandlerError::Reject("initialize may only be sent once")); } let response = request.success(ResponseBody::Initialize(Capabilities { @@ -386,7 +379,7 @@ impl DapSession { self.server.respond(response)?; self.server.send_event(Event::Initialized)?; self.state = DapState::Initialized; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } /// FIXME: distinguish step-over from step-in once Priroda has call-aware stepping. @@ -395,11 +388,12 @@ impl DapSession { request: Request, body: ResponseBody, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - match self.check_step_request(&request) { - Ok(DispatchOutcome::Continue) => {} - Ok(other) => return interp_ok(Ok(other)), - Err(err) => return interp_ok(Err(err)), + ) -> InterpResult<'tcx, Result> { + if let Err(msg) = self.require_stopped() { + return interp_ok(Err(HandlerError::Reject(msg))); + } + if let Err(msg) = Self::require_thread_id(&request) { + return interp_ok(Err(HandlerError::Reject(msg))); } match Self::execution_outcome(session.step()) { @@ -411,7 +405,8 @@ impl DapSession { self.state = DapState::Stopped; self.send_stopped_event(Self::stopped_reason(result)) }) - .map(|()| DispatchOutcome::Continue), + .map(|()| HandlerOutcome::Continue) + .map_err(HandlerError::Transport), ), ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, body, code)), @@ -424,11 +419,12 @@ impl DapSession { &mut self, request: Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - match self.check_step_request(&request) { - Ok(DispatchOutcome::Continue) => {} - Ok(other) => return interp_ok(Ok(other)), - Err(err) => return interp_ok(Err(err)), + ) -> InterpResult<'tcx, Result> { + if let Err(msg) = self.require_stopped() { + return interp_ok(Err(HandlerError::Reject(msg))); + } + if let Err(msg) = Self::require_thread_id(&request) { + return interp_ok(Err(HandlerError::Reject(msg))); } let body = ResponseBody::Continue(ContinueResponse { all_threads_continued: Some(true) }); @@ -442,7 +438,8 @@ impl DapSession { self.state = DapState::Stopped; self.send_stopped_event(Self::stopped_reason(result)) }) - .map(|()| DispatchOutcome::Continue), + .map(|()| HandlerOutcome::Continue) + .map_err(HandlerError::Transport), ), ExecutionOutcome::Terminated { code } => interp_ok(self.respond_terminated(request, body, code)), @@ -456,13 +453,11 @@ impl DapSession { request: Request, args: &SetBreakpointsArguments, session: &mut PrirodaContext<'tcx>, - ) -> Result { - if let Err(msg) = self.reject_after_termination() { - return Ok(DispatchOutcome::Rejected(msg)); - } + ) -> Result { + self.reject_after_termination().map_err(HandlerError::Reject)?; let Some(ref path_str) = args.source.path else { - return Ok(DispatchOutcome::Rejected( + return Err(HandlerError::Reject( "setBreakpoints requires a source.path; sourceReference loads are not supported", )); }; @@ -491,27 +486,27 @@ impl DapSession { let response = request.success(ResponseBody::SetBreakpoints(SetBreakpointsResponse { breakpoints })); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } - fn handle_disconnect(&mut self, request: Request) -> Result { + fn handle_disconnect(&mut self, request: Request) -> Result { self.server.respond(request.success(ResponseBody::Disconnect))?; self.state = DapState::Terminated; self.server.send_event(Event::Terminated(None))?; - Ok(DispatchOutcome::Exit) + Ok(HandlerOutcome::Exit) } fn handle_unsupported_request( &mut self, request: Request, - ) -> Result { + ) -> Result { let message = format!( "unsupported request in Priroda DAP demo mode: {}", Self::display_command(&request.command) ); let response = request.error(&message); self.server.respond(response)?; - Ok(DispatchOutcome::Continue) + Ok(HandlerOutcome::Continue) } fn reject_after_termination(&self) -> Result<(), &'static str> { @@ -575,34 +570,15 @@ impl DapSession { Ok(()) } - fn check_configuration_done_request(&self) -> Result { - if let Err(msg) = self.require_state(DapState::Launched) { - return Ok(DispatchOutcome::Rejected(msg)); - } - - Ok(DispatchOutcome::Continue) - } - - fn check_step_request(&self, request: &Request) -> Result { - if let Err(msg) = self.require_stopped() { - return Ok(DispatchOutcome::Rejected(msg)); - } - if let Err(msg) = Self::require_thread_id(request) { - return Ok(DispatchOutcome::Rejected(msg)); - } - - Ok(DispatchOutcome::Continue) - } - fn respond_execution_error( &mut self, request: Request, message: String, - ) -> Result { + ) -> Result { self.state = DapState::Terminated; self.server.respond(request.error(&message))?; self.server.send_event(Event::Terminated(None))?; - Ok(DispatchOutcome::Exit) + Ok(HandlerOutcome::Exit) } fn respond_terminated( @@ -610,12 +586,12 @@ impl DapSession { request: Request, body: ResponseBody, code: i32, - ) -> Result { + ) -> Result { self.state = DapState::Terminated; self.server.respond(request.success(body))?; self.server.send_event(Event::Exited(ExitedEventBody { exit_code: code.into() }))?; self.server.send_event(Event::Terminated(None))?; - Ok(DispatchOutcome::Exit) + Ok(HandlerOutcome::Exit) } fn execution_outcome<'tcx>(result: InterpResult<'tcx, StepResult>) -> ExecutionOutcome { From a17c6d56bf1947f0991901c1a8c6fce4af4adbba Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 18:08:13 +0300 Subject: [PATCH 38/42] [Priroda] pass thread_id by value into require_thread_id require_thread_id now takes i64. Callers already know which command they are handling, so they pull thread_id directly. This was the last predicate that took &Request. Inlined require_initialized at its one callsite, single matches! check, no point keeping it separate. The dispatch extraction arms use bug!("wrong command") for the impossible fallback, matching the existing bug! style in the file. --- src/tools/miri/priroda/src/frontend/dap.rs | 42 +++++++++++----------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index b110e90a4fb1e..a44e462a16d66 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -122,8 +122,8 @@ impl DapSession { request: Request, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx, Result> { - if let Err(msg) = self.require_initialized(&request) { - return interp_ok(Err(HandlerError::Reject(msg))); + if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { + return interp_ok(Err(HandlerError::Reject("initialize must be sent first"))); } let outcome = match &request.command { @@ -317,8 +317,12 @@ impl DapSession { request: Request, session: &PrirodaContext<'tcx>, ) -> Result { + let thread_id = match &request.command { + Command::StackTrace(args) => args.thread_id, + _ => bug!("wrong command"), + }; self.require_stopped().map_err(HandlerError::Reject)?; - Self::require_thread_id(&request).map_err(HandlerError::Reject)?; + Self::require_thread_id(thread_id).map_err(HandlerError::Reject)?; let stack_frames = match &session.current_location { Some(location) => { @@ -392,7 +396,12 @@ impl DapSession { if let Err(msg) = self.require_stopped() { return interp_ok(Err(HandlerError::Reject(msg))); } - if let Err(msg) = Self::require_thread_id(&request) { + let thread_id = match &request.command { + Command::Next(args) => args.thread_id, + Command::StepIn(args) => args.thread_id, + _ => bug!("wrong command"), + }; + if let Err(msg) = Self::require_thread_id(thread_id) { return interp_ok(Err(HandlerError::Reject(msg))); } @@ -423,7 +432,11 @@ impl DapSession { if let Err(msg) = self.require_stopped() { return interp_ok(Err(HandlerError::Reject(msg))); } - if let Err(msg) = Self::require_thread_id(&request) { + let thread_id = match &request.command { + Command::Continue(args) => args.thread_id, + _ => bug!("wrong command"), + }; + if let Err(msg) = Self::require_thread_id(thread_id) { return interp_ok(Err(HandlerError::Reject(msg))); } @@ -527,13 +540,6 @@ impl DapSession { Ok(()) } - fn require_initialized(&self, request: &Request) -> Result<(), &'static str> { - if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { - return Err("initialize must be sent first"); - } - Ok(()) - } - fn require_stopped(&self) -> Result<(), &'static str> { if self.state != DapState::Stopped { return Err("request requires a stopped frame"); @@ -541,16 +547,8 @@ impl DapSession { Ok(()) } - fn require_thread_id(request: &Request) -> Result<(), &'static str> { - let valid = match &request.command { - Command::StackTrace(args) => args.thread_id == THREAD_ID, - Command::Next(args) => args.thread_id == THREAD_ID, - Command::StepIn(args) => args.thread_id == THREAD_ID, - Command::Continue(args) => args.thread_id == THREAD_ID, - _ => true, - }; - - if !valid { + fn require_thread_id(thread_id: i64) -> Result<(), &'static str> { + if thread_id != THREAD_ID { return Err("unknown threadId"); } Ok(()) From ed18aeaa68718aaa04ac03e542e2bbe70b1c8e75 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Wed, 5 Aug 2026 19:08:39 +0300 Subject: [PATCH 39/42] [Priroda] centralize DAP response sends in run_requests Handlers no longer take Request or call request.success/error themselves; they return HandlerSuccess { response, state, events, outcome } and run_requests is the single send site for both success and error responses, applying state transitions and forwarding events in emitted order. Drop respond_terminated and respond_execution_error -- their response/event construction moves inline at the ExecutionOutcome match arms. send_stopped_event becomes stopped_event_body (pure). dispatch_request takes &Request instead of owning+cloning; handlers receive already-destructured args. handle_unsupported_request takes &Command. HandlerError is gone; handlers return Result so predicate errors bubble via plain ?. Note: state mutations now happen after the response send, not before. If a transport write fails, state is left untouched rather than half-mutated. Wire output is unchanged for the success path. --- src/tools/miri/priroda/src/frontend/dap.rs | 507 ++++++++++----------- 1 file changed, 253 insertions(+), 254 deletions(-) diff --git a/src/tools/miri/priroda/src/frontend/dap.rs b/src/tools/miri/priroda/src/frontend/dap.rs index a44e462a16d66..6e48510cadc5c 100644 --- a/src/tools/miri/priroda/src/frontend/dap.rs +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -21,22 +21,23 @@ use crate::debugger::{LocalDesc, PrirodaContext, StepResult}; const THREAD_ID: i64 = 1; const STACK_FRAME_ID: i64 = 1; const LOCALS_VARIABLES_REFERENCE: i64 = 1; -type ServerResult = Result; -enum HandlerOutcome { - Continue, - Exit, +enum HandlerResponse { + Success(ResponseBody), + Error(String), } -enum HandlerError { - Reject(&'static str), - Transport(ServerError), +struct HandlerSuccess { + response: HandlerResponse, + state: Option, + events: Vec, + outcome: HandlerOutcome, } -impl From for HandlerError { - fn from(e: ServerError) -> Self { - HandlerError::Transport(e) - } +#[derive(Clone, Copy, PartialEq, Eq)] +enum HandlerOutcome { + Continue, + Exit, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -63,7 +64,7 @@ impl Dap { &self, session: &mut PrirodaContext<'tcx>, ) -> InterpResult<'tcx> { - if let Err(err) = DapSession::stdio().run_requests(session)? { + if let Err(err) = DapSession::stdio().run_requests(session) { eprintln!("priroda dap error: {err:?}"); } @@ -93,67 +94,61 @@ impl DapSession { fn run_requests<'tcx>( &mut self, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, ServerResult> { + ) -> Result<(), ServerError> { loop { let request = match self.server.poll_request() { Ok(Some(request)) => request, - Ok(None) => return interp_ok(Ok(())), - Err(err) => return interp_ok(Err(err)), + Ok(None) => return Ok(()), + Err(err) => return Err(err), }; - let request_for_dispatch = request.clone(); - - match self.dispatch_request(request_for_dispatch, session)? { - Ok(HandlerOutcome::Continue) => {} - Ok(HandlerOutcome::Exit) => return interp_ok(Ok(())), - Err(HandlerError::Reject(msg)) => { - let response = request.error(msg); - if let Err(err) = self.server.respond(response) { - return interp_ok(Err(err)); + match self.dispatch_request(&request, session) { + Ok(s) => { + let response = match s.response { + HandlerResponse::Success(body) => request.success(body), + HandlerResponse::Error(message) => request.error(&message), + }; + self.server.respond(response)?; + if let Some(st) = s.state { + self.state = st; + } + for ev in s.events { + self.server.send_event(ev)?; + } + if s.outcome == HandlerOutcome::Exit { + return Ok(()); } } - Err(HandlerError::Transport(e)) => return interp_ok(Err(e)), + Err(msg) => { + self.server.respond(request.error(msg))?; + } } } } fn dispatch_request<'tcx>( - &mut self, - request: Request, + &self, + request: &Request, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { + ) -> Result { if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { - return interp_ok(Err(HandlerError::Reject("initialize must be sent first"))); + return Err("initialize must be sent first"); } - let outcome = match &request.command { - Command::Initialize(_) => self.handle_initialize(request), - Command::Launch(_) => self.handle_launch(request), - Command::ConfigurationDone => return self.handle_configuration_done(request, session), - Command::Threads => self.handle_threads(request), - Command::StackTrace(_) => self.handle_stack_trace(request, session), - Command::Scopes(args) => { - let frame_id = args.frame_id; - self.handle_scopes(request, frame_id, session) - } - Command::Variables(args) => { - let variables_reference = args.variables_reference; - self.handle_variables(request, variables_reference, session) - } - Command::Continue(_) => return self.handle_continue(request, session), - Command::SetBreakpoints(args) => { - let args = args.clone(); - self.handle_set_breakpoints(request, &args, session) - } - Command::Next(_) | Command::StepIn(_) => { - let body = match &request.command { - Command::Next(_) => ResponseBody::Next, - Command::StepIn(_) => ResponseBody::StepIn, - _ => bug!("step body is selected by the outer Next/StepIn match"), - }; - return self.handle_step(request, body, session); - } - Command::Disconnect(_) => self.handle_disconnect(request), + match &request.command { + Command::Initialize(_) => self.handle_initialize(), + Command::Launch(_) => self.handle_launch(), + Command::ConfigurationDone => self.handle_configuration_done(session), + Command::Threads => self.handle_threads(), + Command::StackTrace(args) => self.handle_stack_trace(args.thread_id, session), + Command::Scopes(args) => self.handle_scopes(args.frame_id, session), + Command::Variables(args) => self.handle_variables(args.variables_reference, session), + Command::Continue(args) => self.handle_continue(args.thread_id, session), + Command::SetBreakpoints(args) => self.handle_set_breakpoints(args, session), + Command::Next(args) => self.handle_step(ResponseBody::Next, args.thread_id, session), + Command::StepIn(args) => + self.handle_step(ResponseBody::StepIn, args.thread_id, session), + Command::Disconnect(_) => self.handle_disconnect(), Command::Attach(_) | Command::BreakpointLocations(_) | Command::Cancel(_) @@ -183,29 +178,29 @@ impl DapSession { | Command::StepOut(_) | Command::Terminate(_) | Command::TerminateThreads(_) - | Command::WriteMemory(_) => self.handle_unsupported_request(request), - }; - interp_ok(outcome) + | Command::WriteMemory(_) => self.handle_unsupported_request(&request.command), + } } /// FIXME: connect launch arguments to Priroda's session model. - fn handle_launch(&mut self, request: Request) -> Result { - self.require_state(DapState::Initialized).map_err(HandlerError::Reject)?; + fn handle_launch(&self) -> Result { + self.require_state(DapState::Initialized)?; - let response = request.success(ResponseBody::Launch); - self.server.respond(response)?; - self.state = DapState::Launched; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Launch), + state: Some(DapState::Launched), + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } fn handle_scopes<'tcx>( - &mut self, - request: Request, + &self, frame_id: i64, session: &PrirodaContext<'tcx>, - ) -> Result { - self.require_stopped().map_err(HandlerError::Reject)?; - Self::require_frame_id(frame_id).map_err(HandlerError::Reject)?; + ) -> Result { + self.require_stopped()?; + Self::require_frame_id(frame_id)?; let (source, line, column) = match &session.current_location { Some(location) => { @@ -230,33 +225,35 @@ impl DapSession { } None => (None, None, None), }; - let response = request.success(ResponseBody::Scopes(ScopesResponse { - scopes: vec![Scope { - name: "Locals".to_string(), - presentation_hint: Some(ScopePresentationhint::Locals), - variables_reference: LOCALS_VARIABLES_REFERENCE, - named_variables: None, - indexed_variables: Some(0), - expensive: false, - source, - line, - column, - end_line: None, - end_column: None, - }], - })); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Scopes(ScopesResponse { + scopes: vec![Scope { + name: "Locals".to_string(), + presentation_hint: Some(ScopePresentationhint::Locals), + variables_reference: LOCALS_VARIABLES_REFERENCE, + named_variables: None, + indexed_variables: Some(0), + expensive: false, + source, + line, + column, + end_line: None, + end_column: None, + }], + })), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } fn handle_variables<'tcx>( - &mut self, - request: Request, + &self, variables_reference: i64, session: &PrirodaContext<'tcx>, - ) -> Result { - self.require_stopped().map_err(HandlerError::Reject)?; - Self::require_variables_reference(variables_reference).map_err(HandlerError::Reject)?; + ) -> Result { + self.require_stopped()?; + Self::require_variables_reference(variables_reference)?; let variables = if variables_reference == LOCALS_VARIABLES_REFERENCE { session.list_locals().into_iter().map(Self::local_to_variable).collect() @@ -264,65 +261,75 @@ impl DapSession { Vec::new() }; - let response = request.success(ResponseBody::Variables(VariablesResponse { variables })); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Variables(VariablesResponse { + variables, + })), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } fn handle_configuration_done<'tcx>( - &mut self, - request: Request, + &self, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - if let Err(msg) = self.require_state(DapState::Launched) { - return interp_ok(Err(HandlerError::Reject(msg))); - } + ) -> Result { + self.require_state(DapState::Launched)?; match Self::execution_outcome(session.stop_at_first_user_location()) { - ExecutionOutcome::Stopped(_) => { - let response = request.success(ResponseBody::ConfigurationDone); - interp_ok( - self.server - .respond(response) - .and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(StoppedEventReason::Entry) - }) - .map(|()| HandlerOutcome::Continue) - .map_err(HandlerError::Transport), - ) - } + ExecutionOutcome::Stopped(_) => + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::ConfigurationDone), + state: Some(DapState::Stopped), + events: vec![Event::Stopped(Self::stopped_event_body( + StoppedEventReason::Entry, + ))], + outcome: HandlerOutcome::Continue, + }), ExecutionOutcome::Terminated { code } => - interp_ok(self.respond_terminated(request, ResponseBody::ConfigurationDone, code)), + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::ConfigurationDone), + state: Some(DapState::Terminated), + events: vec![ + Event::Exited(ExitedEventBody { exit_code: code.into() }), + Event::Terminated(None), + ], + outcome: HandlerOutcome::Exit, + }), ExecutionOutcome::Failed(message) => - interp_ok(self.respond_execution_error(request, message)), + Ok(HandlerSuccess { + response: HandlerResponse::Error(message), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }), } } /// FIXME: replace this with Miri thread state once Priroda exposes a /// frontend-facing thread model. - fn handle_threads(&mut self, request: Request) -> Result { - self.reject_after_termination().map_err(HandlerError::Reject)?; - - let response = request.success(ResponseBody::Threads(ThreadsResponse { - threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], - })); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + fn handle_threads(&self) -> Result { + self.reject_after_termination()?; + + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Threads(ThreadsResponse { + threads: vec![Thread { id: THREAD_ID, name: "main".to_string() }], + })), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } /// FIXME: report all frames once Priroda exposes a frontend-facing stack model. fn handle_stack_trace<'tcx>( - &mut self, - request: Request, + &self, + thread_id: i64, session: &PrirodaContext<'tcx>, - ) -> Result { - let thread_id = match &request.command { - Command::StackTrace(args) => args.thread_id, - _ => bug!("wrong command"), - }; - self.require_stopped().map_err(HandlerError::Reject)?; - Self::require_thread_id(thread_id).map_err(HandlerError::Reject)?; + ) -> Result { + self.require_stopped()?; + Self::require_thread_id(thread_id)?; let stack_frames = match &session.current_location { Some(location) => { @@ -361,118 +368,126 @@ impl DapSession { }; let total_frames: i64 = stack_frames.len().try_into().unwrap_or_else(|_| bug!("frame count exceeds i64")); - let response = request.success(ResponseBody::StackTrace(StackTraceResponse { - stack_frames, - total_frames: Some(total_frames), - })); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::StackTrace(StackTraceResponse { + stack_frames, + total_frames: Some(total_frames), + })), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } /// FIXME: grow capabilities as Priroda adds DAP features. - fn handle_initialize(&mut self, request: Request) -> Result { + fn handle_initialize(&self) -> Result { if self.state != DapState::Fresh { - return Err(HandlerError::Reject("initialize may only be sent once")); + return Err("initialize may only be sent once"); } - let response = request.success(ResponseBody::Initialize(Capabilities { - supports_configuration_done_request: Some(true), - supports_single_thread_execution_requests: Some(true), - ..Capabilities::default() - })); - self.server.respond(response)?; - self.server.send_event(Event::Initialized)?; - self.state = DapState::Initialized; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Initialize(Capabilities { + supports_configuration_done_request: Some(true), + supports_single_thread_execution_requests: Some(true), + ..Capabilities::default() + })), + state: Some(DapState::Initialized), + events: vec![Event::Initialized], + outcome: HandlerOutcome::Continue, + }) } /// FIXME: distinguish step-over from step-in once Priroda has call-aware stepping. fn handle_step<'tcx>( - &mut self, - request: Request, + &self, body: ResponseBody, + thread_id: i64, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - if let Err(msg) = self.require_stopped() { - return interp_ok(Err(HandlerError::Reject(msg))); - } - let thread_id = match &request.command { - Command::Next(args) => args.thread_id, - Command::StepIn(args) => args.thread_id, - _ => bug!("wrong command"), - }; - if let Err(msg) = Self::require_thread_id(thread_id) { - return interp_ok(Err(HandlerError::Reject(msg))); - } + ) -> Result { + self.require_stopped()?; + Self::require_thread_id(thread_id)?; match Self::execution_outcome(session.step()) { ExecutionOutcome::Stopped(result) => - interp_ok( - self.server - .respond(request.success(body)) - .and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(Self::stopped_reason(result)) - }) - .map(|()| HandlerOutcome::Continue) - .map_err(HandlerError::Transport), - ), + Ok(HandlerSuccess { + response: HandlerResponse::Success(body), + state: Some(DapState::Stopped), + events: vec![Event::Stopped(Self::stopped_event_body(Self::stopped_reason( + result, + )))], + outcome: HandlerOutcome::Continue, + }), ExecutionOutcome::Terminated { code } => - interp_ok(self.respond_terminated(request, body, code)), + Ok(HandlerSuccess { + response: HandlerResponse::Success(body), + state: Some(DapState::Terminated), + events: vec![ + Event::Exited(ExitedEventBody { exit_code: code.into() }), + Event::Terminated(None), + ], + outcome: HandlerOutcome::Exit, + }), ExecutionOutcome::Failed(message) => - interp_ok(self.respond_execution_error(request, message)), + Ok(HandlerSuccess { + response: HandlerResponse::Error(message), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }), } } fn handle_continue<'tcx>( - &mut self, - request: Request, + &self, + thread_id: i64, session: &mut PrirodaContext<'tcx>, - ) -> InterpResult<'tcx, Result> { - if let Err(msg) = self.require_stopped() { - return interp_ok(Err(HandlerError::Reject(msg))); - } - let thread_id = match &request.command { - Command::Continue(args) => args.thread_id, - _ => bug!("wrong command"), - }; - if let Err(msg) = Self::require_thread_id(thread_id) { - return interp_ok(Err(HandlerError::Reject(msg))); - } + ) -> Result { + self.require_stopped()?; + Self::require_thread_id(thread_id)?; let body = ResponseBody::Continue(ContinueResponse { all_threads_continued: Some(true) }); match Self::execution_outcome(session.continue_execution()) { ExecutionOutcome::Stopped(result) => - interp_ok( - self.server - .respond(request.success(body)) - .and_then(|()| { - self.state = DapState::Stopped; - self.send_stopped_event(Self::stopped_reason(result)) - }) - .map(|()| HandlerOutcome::Continue) - .map_err(HandlerError::Transport), - ), + Ok(HandlerSuccess { + response: HandlerResponse::Success(body), + state: Some(DapState::Stopped), + events: vec![Event::Stopped(Self::stopped_event_body(Self::stopped_reason( + result, + )))], + outcome: HandlerOutcome::Continue, + }), ExecutionOutcome::Terminated { code } => - interp_ok(self.respond_terminated(request, body, code)), + Ok(HandlerSuccess { + response: HandlerResponse::Success(body), + state: Some(DapState::Terminated), + events: vec![ + Event::Exited(ExitedEventBody { exit_code: code.into() }), + Event::Terminated(None), + ], + outcome: HandlerOutcome::Exit, + }), ExecutionOutcome::Failed(message) => - interp_ok(self.respond_execution_error(request, message)), + Ok(HandlerSuccess { + response: HandlerResponse::Error(message), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }), } } fn handle_set_breakpoints<'tcx>( - &mut self, - request: Request, + &self, args: &SetBreakpointsArguments, session: &mut PrirodaContext<'tcx>, - ) -> Result { - self.reject_after_termination().map_err(HandlerError::Reject)?; + ) -> Result { + self.reject_after_termination()?; let Some(ref path_str) = args.source.path else { - return Err(HandlerError::Reject( + return Err( "setBreakpoints requires a source.path; sourceReference loads are not supported", - )); + ); }; let path = std::path::PathBuf::from(path_str); @@ -496,30 +511,38 @@ impl DapSession { } } - let response = - request.success(ResponseBody::SetBreakpoints(SetBreakpointsResponse { breakpoints })); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::SetBreakpoints( + SetBreakpointsResponse { breakpoints }, + )), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } - fn handle_disconnect(&mut self, request: Request) -> Result { - self.server.respond(request.success(ResponseBody::Disconnect))?; - self.state = DapState::Terminated; - self.server.send_event(Event::Terminated(None))?; - Ok(HandlerOutcome::Exit) + fn handle_disconnect(&self) -> Result { + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Disconnect), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }) } fn handle_unsupported_request( - &mut self, - request: Request, - ) -> Result { - let message = format!( - "unsupported request in Priroda DAP demo mode: {}", - Self::display_command(&request.command) - ); - let response = request.error(&message); - self.server.respond(response)?; - Ok(HandlerOutcome::Continue) + &self, + command: &Command, + ) -> Result { + Ok(HandlerSuccess { + response: HandlerResponse::Error(format!( + "unsupported request in Priroda DAP demo mode: {}", + Self::display_command(command) + )), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) } fn reject_after_termination(&self) -> Result<(), &'static str> { @@ -568,30 +591,6 @@ impl DapSession { Ok(()) } - fn respond_execution_error( - &mut self, - request: Request, - message: String, - ) -> Result { - self.state = DapState::Terminated; - self.server.respond(request.error(&message))?; - self.server.send_event(Event::Terminated(None))?; - Ok(HandlerOutcome::Exit) - } - - fn respond_terminated( - &mut self, - request: Request, - body: ResponseBody, - code: i32, - ) -> Result { - self.state = DapState::Terminated; - self.server.respond(request.success(body))?; - self.server.send_event(Event::Exited(ExitedEventBody { exit_code: code.into() }))?; - self.server.send_event(Event::Terminated(None))?; - Ok(HandlerOutcome::Exit) - } - fn execution_outcome<'tcx>(result: InterpResult<'tcx, StepResult>) -> ExecutionOutcome { match result.report_err() { Ok(step) => ExecutionOutcome::Stopped(step), @@ -610,8 +609,8 @@ impl DapSession { ExecutionOutcome::Failed(kind.to_string()) } - fn send_stopped_event(&mut self, reason: StoppedEventReason) -> ServerResult { - self.server.send_event(Event::Stopped(StoppedEventBody { + fn stopped_event_body(reason: StoppedEventReason) -> StoppedEventBody { + StoppedEventBody { reason, description: None, thread_id: Some(THREAD_ID), @@ -619,7 +618,7 @@ impl DapSession { text: None, all_threads_stopped: Some(true), hit_breakpoint_ids: None, - })) + } } fn stopped_reason(result: StepResult) -> StoppedEventReason { From 2d66ef1d5594d09b1e429c4573003d456b0fded7 Mon Sep 17 00:00:00 2001 From: hkalbasi Date: Sun, 26 Jul 2026 19:28:55 +0330 Subject: [PATCH 40/42] Lookup exported statics when encountering an unsupported imported static --- src/tools/miri/src/machine.rs | 63 +++++++++++++- src/tools/miri/src/shims/foreign_items.rs | 63 +++++++++----- src/tools/miri/src/shims/sig.rs | 52 ++++++------ .../miri/tests/fail/extern_static/clashing.rs | 15 ++++ .../tests/fail/extern_static/clashing.stderr | 21 +++++ .../in_const.rs} | 0 .../in_const.stderr} | 2 +- .../tests/fail/extern_static/mut_mismatch1.rs | 13 +++ .../fail/extern_static/mut_mismatch1.stderr | 13 +++ .../tests/fail/extern_static/mut_mismatch2.rs | 17 ++++ .../fail/extern_static/mut_mismatch2.stderr | 13 +++ .../tests/fail/extern_static/mut_mismatch3.rs | 13 +++ .../fail/extern_static/mut_mismatch3.stderr | 13 +++ .../fail/extern_static/shim_clashing1.rs | 15 ++++ .../fail/extern_static/shim_clashing1.stderr | 16 ++++ .../fail/extern_static/shim_clashing2.rs | 13 +++ .../fail/extern_static/shim_clashing2.stderr | 16 ++++ .../fail/extern_static/type_confusion.rs | 14 ++++ .../fail/extern_static/type_confusion.stderr | 13 +++ .../unsupported.rs} | 0 .../unsupported.stderr} | 2 +- .../fail/extern_static/write_immutable.rs | 29 +++++++ .../fail/extern_static/write_immutable.stderr | 13 +++ .../tests/fail/extern_static/wrong_size.rs | 10 +++ .../fail/extern_static/wrong_size.stderr | 13 +++ .../wrong_size_shim.rs} | 0 .../wrong_size_shim.stderr} | 4 +- .../tests/fail/extern_static/wrong_type.rs | 11 +++ .../fail/extern_static/wrong_type.stderr | 13 +++ .../exported_symbol_shim_clashing.stderr | 7 +- src/tools/miri/tests/pass/extern_static.rs | 83 +++++++++++++++++++ 31 files changed, 511 insertions(+), 59 deletions(-) create mode 100644 src/tools/miri/tests/fail/extern_static/clashing.rs create mode 100644 src/tools/miri/tests/fail/extern_static/clashing.stderr rename src/tools/miri/tests/fail/{extern_static_in_const.rs => extern_static/in_const.rs} (100%) rename src/tools/miri/tests/fail/{extern_static_in_const.stderr => extern_static/in_const.stderr} (89%) create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch1.rs create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch1.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch2.rs create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch2.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch3.rs create mode 100644 src/tools/miri/tests/fail/extern_static/mut_mismatch3.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/shim_clashing1.rs create mode 100644 src/tools/miri/tests/fail/extern_static/shim_clashing1.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/shim_clashing2.rs create mode 100644 src/tools/miri/tests/fail/extern_static/shim_clashing2.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/type_confusion.rs create mode 100644 src/tools/miri/tests/fail/extern_static/type_confusion.stderr rename src/tools/miri/tests/fail/{extern_static.rs => extern_static/unsupported.rs} (100%) rename src/tools/miri/tests/fail/{extern_static.stderr => extern_static/unsupported.stderr} (90%) create mode 100644 src/tools/miri/tests/fail/extern_static/write_immutable.rs create mode 100644 src/tools/miri/tests/fail/extern_static/write_immutable.stderr create mode 100644 src/tools/miri/tests/fail/extern_static/wrong_size.rs create mode 100644 src/tools/miri/tests/fail/extern_static/wrong_size.stderr rename src/tools/miri/tests/fail/{extern_static_wrong_size.rs => extern_static/wrong_size_shim.rs} (100%) rename src/tools/miri/tests/fail/{extern_static_wrong_size.stderr => extern_static/wrong_size_shim.stderr} (65%) create mode 100644 src/tools/miri/tests/fail/extern_static/wrong_type.rs create mode 100644 src/tools/miri/tests/fail/extern_static/wrong_type.stderr create mode 100644 src/tools/miri/tests/pass/extern_static.rs diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs index f476614992041..3ca5208505205 100644 --- a/src/tools/miri/src/machine.rs +++ b/src/tools/miri/src/machine.rs @@ -12,12 +12,14 @@ use rand::rngs::StdRng; use rand::{RngExt, SeedableRng}; use rustc_abi::{Align, ExternAbi, Size}; use rustc_apfloat::{Float, FloatConvert}; +use rustc_ast::Mutability; use rustc_ast::expand::allocator::{self, SpecialAllocatorMethod}; use rustc_data_structures::either::Either; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; #[allow(unused)] use rustc_data_structures::static_assert_size; use rustc_hir::attrs::{InlineAttr, Linkage}; +use rustc_hir::def::DefKind; use rustc_log::tracing; use rustc_middle::middle::codegen_fn_attrs::TargetFeatureKind; use rustc_middle::mir; @@ -566,7 +568,7 @@ pub struct MiriMachine<'tcx> { /// Cache of `Instance` exported under the given `Symbol` name. /// `None` means no `Instance` exported under the given name is found. - pub(crate) exported_symbols_cache: FxHashMap>>, + pub(crate) exported_symbols_cache: RefCell>>>, /// Equivalent setting as RUST_BACKTRACE on encountering an error. pub(crate) backtrace_style: BacktraceStyle, @@ -776,7 +778,7 @@ impl<'tcx> MiriMachine<'tcx> { static_roots: Vec::new(), profiler, string_cache: Default::default(), - exported_symbols_cache: FxHashMap::default(), + exported_symbols_cache: RefCell::new(FxHashMap::default()), backtrace_style: config.backtrace_style, user_relevant_crates, extern_statics: FxHashMap::default(), @@ -1462,6 +1464,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { Some(_) => ecx.machine.extern_statics_imports.get(&link_name), }; if let Some(&ptr) = ptr { + ecx.check_shim_symbol_clash(link_name)?; // Various parts of the engine rely on `get_alloc_info` for size and alignment // information. That uses the type information of this static. // Make sure it matches the Miri allocation for this. @@ -1503,7 +1506,61 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { .expect("`missing_weak_symbol` should have been initialized"), ) } else { - throw_unsup_format!("extern static `{link_name}` is not supported by Miri") + // Look for a Rust static with this symbol name in the crate graph. + let Some(instance) = ecx.lookup_exported_static(link_name)? else { + throw_unsup_format!("extern static `{link_name}` is not supported by Miri"); + }; + // Evaluate the static to get its allocation. + let place = ecx.eval_global(instance)?; + let static_ptr = place.ptr().into_pointer_or_addr().unwrap(); + // Validate the allocation matches the declared size and alignment. + let alloc_id = static_ptr.provenance.get_alloc_id().unwrap(); + let info = ecx.get_alloc_info(alloc_id); + if extern_decl_layout.size != info.size || extern_decl_layout.align.abi != info.align { + throw_ub_format!( + "extern static `{link_name}` has been declared as `{krate}::{name}` \ + with a size of {decl_size} bytes and alignment of {decl_align} bytes, \ + but the exported static with that name has a size of {shim_size} bytes and \ + alignment of {shim_align} bytes", + name = ecx.tcx.def_path_str(def_id), + krate = ecx.tcx.crate_name(def_id.krate), + decl_size = extern_decl_layout.size.bytes(), + decl_align = extern_decl_layout.align.bytes(), + shim_size = info.size.bytes(), + shim_align = info.align.bytes(), + ) + } + // Check that the mutability of the declared static matches that of the backing. + // If the backing static can be modified (because it is a `static mut`, or because + // it is a `static` whose type has interior mutability) while the declaration here + // is a non-mut `static` with a `Freeze` type, then the compiler's assumption that + // the value never changes may be violated, so this may cause UB. + // This is somehow defensive, as the allocation might be mutable but no mutation + // ever happens, but this is probably the most precise thing we can do. + // Specially, the second case is very defensive and we may be able to lift it. + let DefKind::Static { mutability, .. } = ecx.tcx.def_kind(def_id) else { + unreachable!("`{def_id:?}` is not a static"); + }; + let decl_is_mut = + !(mutability == Mutability::Not && ecx.type_is_freeze(extern_decl_layout.ty)); + let backing_is_mut = ecx.get_alloc_mutability(alloc_id)? == Mutability::Mut; + if !decl_is_mut && backing_is_mut { + throw_ub_format!( + "extern static `{krate}::{name}` is declared as an immutable `static`, \ + but the backing static is mutable", + name = ecx.tcx.def_path_str(def_id), + krate = ecx.tcx.crate_name(def_id.krate), + ) + } + if decl_is_mut && !backing_is_mut { + throw_ub_format!( + "extern static `{krate}::{name}` is declared as an mutable `static`, \ + but the backing static is immutable", + name = ecx.tcx.def_path_str(def_id), + krate = ecx.tcx.crate_name(def_id.krate), + ) + } + interp_ok(static_ptr) } } diff --git a/src/tools/miri/src/shims/foreign_items.rs b/src/tools/miri/src/shims/foreign_items.rs index a904116017876..80f5369a9bbc1 100644 --- a/src/tools/miri/src/shims/foreign_items.rs +++ b/src/tools/miri/src/shims/foreign_items.rs @@ -57,7 +57,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { match *shim { Either::Left(other_fn) => { let handler = this - .lookup_exported_symbol(other_fn)? + .lookup_exported_fn(other_fn)? .expect("missing alloc error handler symbol"); return interp_ok(Some(handler)); } @@ -75,7 +75,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // The rest either implements the logic, or falls back to `lookup_exported_symbol`. let res = this.emulate_foreign_item_inner(link_name, abi, args, &dest)?; res.jump_to_next_block(this, &dest.clone().into(), ret, Some(unwind), |this| { - if let Some(body) = this.lookup_exported_symbol(link_name)? { + if let Some(body) = this.lookup_exported_fn(link_name)? { return interp_ok(Some(body)); } @@ -110,17 +110,18 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(()) } - /// Lookup the body of a function that has `link_name` as the symbol name. + /// Lookup the instance that has `link_name` as the symbol name. fn lookup_exported_symbol( - &mut self, + &self, link_name: Symbol, - ) -> InterpResult<'tcx, Option<(&'tcx mir::Body<'tcx>, ty::Instance<'tcx>)>> { - let this = self.eval_context_mut(); + ) -> InterpResult<'tcx, Option>> { + let this = self.eval_context_ref(); let tcx = this.tcx.tcx; // If the result was cached, just return it. // (Cannot use `or_insert` since the code below might have to throw an error.) - let entry = this.machine.exported_symbols_cache.entry(link_name); + let mut cache = this.machine.exported_symbols_cache.borrow_mut(); + let entry = cache.entry(link_name); let instance = *match entry { Entry::Occupied(e) => e.into_mut(), Entry::Vacant(e) => { @@ -206,25 +207,49 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(()) })?; - // Once we identified the instance corresponding to the symbol, ensure - // it is a function. It is okay to encounter non-functions in the search above - // as long as the final instance we arrive at is a function. - if let Some(SymbolTarget { instance, .. }) = symbol_target { - if !matches!(tcx.def_kind(instance.def_id()), DefKind::Fn | DefKind::AssocFn) { - throw_ub_format!( - "attempt to call an exported symbol that is not defined as a function" - ); - } - } - e.insert(symbol_target.map(|SymbolTarget { instance, .. }| instance)) } }; + drop(cache); + interp_ok(instance) + } + + /// Lookup the body of a function that has `link_name` as the symbol name. + fn lookup_exported_fn( + &self, + link_name: Symbol, + ) -> InterpResult<'tcx, Option<(&'tcx mir::Body<'tcx>, ty::Instance<'tcx>)>> { + let this = self.eval_context_ref(); + let instance = this.lookup_exported_symbol(link_name)?; + if let Some(instance) = &instance { + if !matches!(this.tcx.def_kind(instance.def_id()), DefKind::Fn | DefKind::AssocFn) { + throw_ub_format!( + "attempt to call an exported symbol that is not defined as a function" + ); + } + } match instance { - None => interp_ok(None), // no symbol with this name + None => interp_ok(None), Some(instance) => interp_ok(Some((this.load_mir(instance.def, None)?, instance))), } } + + /// Lookup the instance of a static that has `link_name` as the symbol name. + fn lookup_exported_static( + &self, + link_name: Symbol, + ) -> InterpResult<'tcx, Option>> { + let this = self.eval_context_ref(); + let instance = this.lookup_exported_symbol(link_name)?; + if let Some(instance) = &instance { + if !matches!(this.tcx.def_kind(instance.def_id()), DefKind::Static { .. }) { + throw_ub_format!( + "attempt to access an exported symbol `{link_name}` that is not defined as a static" + ); + } + } + interp_ok(instance) + } } impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {} diff --git a/src/tools/miri/src/shims/sig.rs b/src/tools/miri/src/shims/sig.rs index b0b4bca3f517c..d40e9039f2b60 100644 --- a/src/tools/miri/src/shims/sig.rs +++ b/src/tools/miri/src/shims/sig.rs @@ -200,30 +200,29 @@ fn check_shim_abi<'tcx>( interp_ok(()) } -fn check_shim_symbol_clash<'tcx>( - this: &mut MiriInterpCx<'tcx>, - link_name: Symbol, -) -> InterpResult<'tcx, ()> { - if let Some((body, instance)) = this.lookup_exported_symbol(link_name)? { - // If compiler-builtins is providing the symbol, then don't treat it as a clash. - // We'll use our built-in implementation in `emulate_foreign_item_inner` for increased - // performance. Note that this means we won't catch any undefined behavior in - // compiler-builtins when running other crates, but Miri can still be run on - // compiler-builtins itself (or any crate that uses it as a normal dependency) - if this.tcx.is_compiler_builtins(instance.def_id().krate) { - return interp_ok(()); - } +impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} +pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { + /// Ensure the given symbol is not exported by the program. + fn check_shim_symbol_clash(&self, link_name: Symbol) -> InterpResult<'tcx, ()> { + let this = self.eval_context_ref(); + if let Some(instance) = this.lookup_exported_symbol(link_name)? { + // If compiler-builtins is providing the symbol, then don't treat it as a clash. + // We'll use our built-in implementation in `emulate_foreign_item_inner` for increased + // performance. Note that this means we won't catch any undefined behavior in + // compiler-builtins when running other crates, but Miri can still be run on + // compiler-builtins itself (or any crate that uses it as a normal dependency) + if this.tcx.is_compiler_builtins(instance.def_id().krate) { + return interp_ok(()); + } - throw_machine_stop!(TerminationInfo::SymbolShimClashing { - link_name, - span: body.span.data(), - }) + throw_machine_stop!(TerminationInfo::SymbolShimClashing { + link_name, + span: this.tcx.def_span(instance.def_id()).data(), + }) + } + interp_ok(()) } - interp_ok(()) -} -impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} -pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn check_shim_sig_lenient<'a, const N: usize>( &mut self, abi: &FnAbi<'tcx, Ty<'tcx>>, @@ -231,8 +230,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { link_name: Symbol, args: &'a [OpTy<'tcx>], ) -> InterpResult<'tcx, &'a [OpTy<'tcx>; N]> { - let this = self.eval_context_mut(); - check_shim_symbol_clash(this, link_name)?; + self.check_shim_symbol_clash(link_name)?; if abi.conv != exp_abi { throw_ub_format!( @@ -283,7 +281,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Check everything. check_shim_abi(this, callee_fn_abi, caller_fn_abi)?; - check_shim_symbol_clash(this, link_name)?; + this.check_shim_symbol_clash(link_name)?; // Return arguments. if let Ok(ops) = caller_args.try_into() { @@ -304,8 +302,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { where &'a [OpTy<'tcx>; N]: TryFrom<&'a [OpTy<'tcx>]>, { - let this = self.eval_context_mut(); - check_shim_symbol_clash(this, link_name)?; + self.check_shim_symbol_clash(link_name)?; if abi.conv != exp_abi { throw_ub_format!( @@ -342,8 +339,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { ) -> InterpResult<'tcx, &'a [OpTy<'tcx>; N]> { assert!(link_name.as_str().starts_with("llvm.")); - let this = self.eval_context_mut(); - check_shim_symbol_clash(this, link_name)?; + self.check_shim_symbol_clash(link_name)?; if let Ok(ops) = args.try_into() { return interp_ok(ops); diff --git a/src/tools/miri/tests/fail/extern_static/clashing.rs b/src/tools/miri/tests/fail/extern_static/clashing.rs new file mode 100644 index 0000000000000..266f4deb1aa83 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/clashing.rs @@ -0,0 +1,15 @@ +#[no_mangle] +static FOO: u8 = 1; +//~^ HELP: it's first defined here, in crate `clashing` + +#[export_name = "FOO"] +static BAR: u8 = 2; +//~^ HELP: then it's defined here again, in crate `clashing` + +fn main() { + extern "Rust" { + static FOO: u8; + } + let _val = &raw const FOO; + //~^ ERROR: multiple definitions of symbol `FOO` +} diff --git a/src/tools/miri/tests/fail/extern_static/clashing.stderr b/src/tools/miri/tests/fail/extern_static/clashing.stderr new file mode 100644 index 0000000000000..0c0c362639ac2 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/clashing.stderr @@ -0,0 +1,21 @@ +error: multiple definitions of symbol `FOO` + --> tests/fail/extern_static/clashing.rs:LL:CC + | +LL | let _val = &raw const FOO; + | ^^^ error occurred here + | +help: it's first defined here, in crate `clashing` + --> tests/fail/extern_static/clashing.rs:LL:CC + | +LL | static FOO: u8 = 1; + | ^^^^^^^^^^^^^^ +help: then it's defined here again, in crate `clashing` + --> tests/fail/extern_static/clashing.rs:LL:CC + | +LL | static BAR: u8 = 2; + | ^^^^^^^^^^^^^^ + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static_in_const.rs b/src/tools/miri/tests/fail/extern_static/in_const.rs similarity index 100% rename from src/tools/miri/tests/fail/extern_static_in_const.rs rename to src/tools/miri/tests/fail/extern_static/in_const.rs diff --git a/src/tools/miri/tests/fail/extern_static_in_const.stderr b/src/tools/miri/tests/fail/extern_static/in_const.stderr similarity index 89% rename from src/tools/miri/tests/fail/extern_static_in_const.stderr rename to src/tools/miri/tests/fail/extern_static/in_const.stderr index f0f0966ea8afe..7cbc11bc6e4cf 100644 --- a/src/tools/miri/tests/fail/extern_static_in_const.stderr +++ b/src/tools/miri/tests/fail/extern_static/in_const.stderr @@ -1,5 +1,5 @@ error: unsupported operation: extern static `E` is not supported by Miri - --> tests/fail/extern_static_in_const.rs:LL:CC + --> tests/fail/extern_static/in_const.rs:LL:CC | LL | let _val = X; | ^ unsupported operation occurred here diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch1.rs b/src/tools/miri/tests/fail/extern_static/mut_mismatch1.rs new file mode 100644 index 0000000000000..02340b0c94dc5 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch1.rs @@ -0,0 +1,13 @@ +//! We want to reserve rights to be able to optimize statics declared as immutable, +//! so we defensively disallow immutable statics pointing to mutable allocations. + +#[export_name = "S"] +static mut BACKING_S: i32 = 42; + +fn main() { + extern "C" { + static S: i32; + } + let _val = &raw const S; + //~^ ERROR: is declared as an immutable `static`, but the backing static is mutable +} diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch1.stderr b/src/tools/miri/tests/fail/extern_static/mut_mismatch1.stderr new file mode 100644 index 0000000000000..11c16810edb9d --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch1.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: extern static `mut_mismatch1::main::S` is declared as an immutable `static`, but the backing static is mutable + --> tests/fail/extern_static/mut_mismatch1.rs:LL:CC + | +LL | let _val = &raw const S; + | ^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch2.rs b/src/tools/miri/tests/fail/extern_static/mut_mismatch2.rs new file mode 100644 index 0000000000000..d2c44850f983e --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch2.rs @@ -0,0 +1,17 @@ +//! We want to reserve rights to be able to optimize statics declared as immutable, +//! so we defensively disallow immutable statics pointing to mutable allocations. + +#![feature(sync_unsafe_cell)] + +use std::cell::SyncUnsafeCell; + +#[export_name = "S"] +static INTERIOR_MUT_S: SyncUnsafeCell = SyncUnsafeCell::new(42); + +fn main() { + extern "C" { + static S: i32; + } + let _val = &raw const S; + //~^ ERROR: is declared as an immutable `static`, but the backing static is mutable +} diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch2.stderr b/src/tools/miri/tests/fail/extern_static/mut_mismatch2.stderr new file mode 100644 index 0000000000000..8d7d886c10032 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch2.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: extern static `mut_mismatch2::main::S` is declared as an immutable `static`, but the backing static is mutable + --> tests/fail/extern_static/mut_mismatch2.rs:LL:CC + | +LL | let _val = &raw const S; + | ^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch3.rs b/src/tools/miri/tests/fail/extern_static/mut_mismatch3.rs new file mode 100644 index 0000000000000..c33066341ff13 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch3.rs @@ -0,0 +1,13 @@ +//! We want to reserve rights to be able to inject implicit writes to mutable declared statics, +//! so we defensively disallow mutable statics pointing to immutable allocations. + +#[export_name = "S"] +static IMMUT_S: i32 = 42; + +fn main() { + extern "C" { + static mut S: i32; + } + let _val = &raw const S; + //~^ ERROR: is declared as an mutable `static`, but the backing static is immutable +} diff --git a/src/tools/miri/tests/fail/extern_static/mut_mismatch3.stderr b/src/tools/miri/tests/fail/extern_static/mut_mismatch3.stderr new file mode 100644 index 0000000000000..dcaff7c4bdf51 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/mut_mismatch3.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: extern static `mut_mismatch3::main::S` is declared as an mutable `static`, but the backing static is immutable + --> tests/fail/extern_static/mut_mismatch3.rs:LL:CC + | +LL | let _val = &raw const S; + | ^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/shim_clashing1.rs b/src/tools/miri/tests/fail/extern_static/shim_clashing1.rs new file mode 100644 index 0000000000000..36bd4e87104f7 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/shim_clashing1.rs @@ -0,0 +1,15 @@ +//@only-target: linux # we need a specific extern supported on this target + +extern "C" { + static mut environ: *const *const u8; +} + +#[export_name = "environ"] +static mut MY_ENVIRON: *const *const u8 = std::ptr::null(); +//~^ HELP: the `environ` symbol is defined here + +fn main() { + let _val = &raw const MY_ENVIRON; + let _val = &raw const environ; + //~^ ERROR: found `environ` symbol definition that clashes with a built-in shim +} diff --git a/src/tools/miri/tests/fail/extern_static/shim_clashing1.stderr b/src/tools/miri/tests/fail/extern_static/shim_clashing1.stderr new file mode 100644 index 0000000000000..59da90d986b39 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/shim_clashing1.stderr @@ -0,0 +1,16 @@ +error: found `environ` symbol definition that clashes with a built-in shim + --> tests/fail/extern_static/shim_clashing1.rs:LL:CC + | +LL | let _val = &raw const environ; + | ^^^^^^^ error occurred here + | +help: the `environ` symbol is defined here + --> tests/fail/extern_static/shim_clashing1.rs:LL:CC + | +LL | static mut MY_ENVIRON: *const *const u8 = std::ptr::null(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/shim_clashing2.rs b/src/tools/miri/tests/fail/extern_static/shim_clashing2.rs new file mode 100644 index 0000000000000..ae3f1380bb386 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/shim_clashing2.rs @@ -0,0 +1,13 @@ +//@only-target: linux # we need a specific extern supported on this target + +#[export_name = "environ"] +fn my_environ() {} +//~^ HELP: the `environ` symbol is defined here + +fn main() { + extern "C" { + static environ: *const *const u8; + } + let _val = &raw const environ; + //~^ ERROR: found `environ` symbol definition that clashes with a built-in shim +} diff --git a/src/tools/miri/tests/fail/extern_static/shim_clashing2.stderr b/src/tools/miri/tests/fail/extern_static/shim_clashing2.stderr new file mode 100644 index 0000000000000..fd5f02a06e85f --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/shim_clashing2.stderr @@ -0,0 +1,16 @@ +error: found `environ` symbol definition that clashes with a built-in shim + --> tests/fail/extern_static/shim_clashing2.rs:LL:CC + | +LL | let _val = &raw const environ; + | ^^^^^^^ error occurred here + | +help: the `environ` symbol is defined here + --> tests/fail/extern_static/shim_clashing2.rs:LL:CC + | +LL | fn my_environ() {} + | ^^^^^^^^^^^^^^^ + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/type_confusion.rs b/src/tools/miri/tests/fail/extern_static/type_confusion.rs new file mode 100644 index 0000000000000..6881cb8c178a7 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/type_confusion.rs @@ -0,0 +1,14 @@ +#[no_mangle] +static FOO: u8 = 42; + +fn main() { + extern "Rust" { + static FOO: bool; + } + // Type confusion between u8 (value 42) and bool: reading as bool is UB + // because 42 is not a valid boolean value (must be 0 or 1). + unsafe { + (&raw const FOO).read(); + //~^ ERROR: /constructing invalid value of type bool/ + } +} diff --git a/src/tools/miri/tests/fail/extern_static/type_confusion.stderr b/src/tools/miri/tests/fail/extern_static/type_confusion.stderr new file mode 100644 index 0000000000000..772f32c3b952b --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/type_confusion.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: constructing invalid value of type bool: encountered 0x2a, but expected a boolean + --> tests/fail/extern_static/type_confusion.rs:LL:CC + | +LL | (&raw const FOO).read(); + | ^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static.rs b/src/tools/miri/tests/fail/extern_static/unsupported.rs similarity index 100% rename from src/tools/miri/tests/fail/extern_static.rs rename to src/tools/miri/tests/fail/extern_static/unsupported.rs diff --git a/src/tools/miri/tests/fail/extern_static.stderr b/src/tools/miri/tests/fail/extern_static/unsupported.stderr similarity index 90% rename from src/tools/miri/tests/fail/extern_static.stderr rename to src/tools/miri/tests/fail/extern_static/unsupported.stderr index e4c51c0345d4c..02a6fdfa5be48 100644 --- a/src/tools/miri/tests/fail/extern_static.stderr +++ b/src/tools/miri/tests/fail/extern_static/unsupported.stderr @@ -1,5 +1,5 @@ error: unsupported operation: extern static `FOO` is not supported by Miri - --> tests/fail/extern_static.rs:LL:CC + --> tests/fail/extern_static/unsupported.rs:LL:CC | LL | let _val = std::ptr::addr_of!(FOO); | ^^^ unsupported operation occurred here diff --git a/src/tools/miri/tests/fail/extern_static/write_immutable.rs b/src/tools/miri/tests/fail/extern_static/write_immutable.rs new file mode 100644 index 0000000000000..d9420ecb86215 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/write_immutable.rs @@ -0,0 +1,29 @@ +//! This test is very similar to `mut_mismatch3`, but actually writes to the static. +//! In case we relaxed `mut_mismatch3` UB, we still want this to remain UB. + +#![feature(sync_unsafe_cell)] + +use std::cell::SyncUnsafeCell; + +#[no_mangle] +static IMMUT: i32 = 42; + +#[no_mangle] +static INTERIOR_MUT: SyncUnsafeCell = SyncUnsafeCell::new(42); + +fn main() { + unsafe { + extern "C" { + static mut INTERIOR_MUT: i32; + } + (&raw mut INTERIOR_MUT).write(7); + } + + unsafe { + extern "C" { + static mut IMMUT: i32; + } + (&raw mut IMMUT).write(7); + //~^ ERROR: is declared as an mutable `static`, but the backing static is immutable + } +} diff --git a/src/tools/miri/tests/fail/extern_static/write_immutable.stderr b/src/tools/miri/tests/fail/extern_static/write_immutable.stderr new file mode 100644 index 0000000000000..74259b36f351d --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/write_immutable.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: extern static `write_immutable::main::IMMUT` is declared as an mutable `static`, but the backing static is immutable + --> tests/fail/extern_static/write_immutable.rs:LL:CC + | +LL | (&raw mut IMMUT).write(7); + | ^^^^^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static/wrong_size.rs b/src/tools/miri/tests/fail/extern_static/wrong_size.rs new file mode 100644 index 0000000000000..d8a53a04df88e --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/wrong_size.rs @@ -0,0 +1,10 @@ +#[no_mangle] +static FOO: u8 = 42; + +fn main() { + extern "Rust" { + static FOO: u16; + } + let _val = unsafe { (&raw const FOO).read() }; + //~^ ERROR: extern static `FOO` has been declared as `wrong_size::main::FOO` with a size of 2 bytes +} diff --git a/src/tools/miri/tests/fail/extern_static/wrong_size.stderr b/src/tools/miri/tests/fail/extern_static/wrong_size.stderr new file mode 100644 index 0000000000000..8cea8376dfd2f --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/wrong_size.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: extern static `FOO` has been declared as `wrong_size::main::FOO` with a size of 2 bytes and alignment of 2 bytes, but the exported static with that name has a size of 1 bytes and alignment of 1 bytes + --> tests/fail/extern_static/wrong_size.rs:LL:CC + | +LL | let _val = unsafe { (&raw const FOO).read() }; + | ^^^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/extern_static_wrong_size.rs b/src/tools/miri/tests/fail/extern_static/wrong_size_shim.rs similarity index 100% rename from src/tools/miri/tests/fail/extern_static_wrong_size.rs rename to src/tools/miri/tests/fail/extern_static/wrong_size_shim.rs diff --git a/src/tools/miri/tests/fail/extern_static_wrong_size.stderr b/src/tools/miri/tests/fail/extern_static/wrong_size_shim.stderr similarity index 65% rename from src/tools/miri/tests/fail/extern_static_wrong_size.stderr rename to src/tools/miri/tests/fail/extern_static/wrong_size_shim.stderr index 0862f97792872..d3a0f0205ee3b 100644 --- a/src/tools/miri/tests/fail/extern_static_wrong_size.stderr +++ b/src/tools/miri/tests/fail/extern_static/wrong_size_shim.stderr @@ -1,5 +1,5 @@ -error: unsupported operation: extern static `environ` has been declared as `extern_static_wrong_size::environ` with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes - --> tests/fail/extern_static_wrong_size.rs:LL:CC +error: unsupported operation: extern static `environ` has been declared as `wrong_size_shim::environ` with a size of 1 bytes and alignment of 1 bytes, but Miri emulates it via an extern static shim with a size of N bytes and alignment of N bytes + --> tests/fail/extern_static/wrong_size_shim.rs:LL:CC | LL | let _val = unsafe { environ }; | ^^^^^^^ unsupported operation occurred here diff --git a/src/tools/miri/tests/fail/extern_static/wrong_type.rs b/src/tools/miri/tests/fail/extern_static/wrong_type.rs new file mode 100644 index 0000000000000..81683b3ee9da2 --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/wrong_type.rs @@ -0,0 +1,11 @@ +#[allow(non_snake_case)] +#[no_mangle] +fn FOO() {} + +fn main() { + extern "Rust" { + static FOO: (); + } + let _val = &raw const FOO; + //~^ ERROR: attempt to access an exported symbol `FOO` that is not defined as a static +} diff --git a/src/tools/miri/tests/fail/extern_static/wrong_type.stderr b/src/tools/miri/tests/fail/extern_static/wrong_type.stderr new file mode 100644 index 0000000000000..ba38acd3ce16b --- /dev/null +++ b/src/tools/miri/tests/fail/extern_static/wrong_type.stderr @@ -0,0 +1,13 @@ +error: Undefined Behavior: attempt to access an exported symbol `FOO` that is not defined as a static + --> tests/fail/extern_static/wrong_type.rs:LL:CC + | +LL | let _val = &raw const FOO; + | ^^^ Undefined Behavior occurred here + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/src/tools/miri/tests/fail/function_calls/exported_symbol_shim_clashing.stderr b/src/tools/miri/tests/fail/function_calls/exported_symbol_shim_clashing.stderr index 0e2b4da5c0a03..5c013b862a7a0 100644 --- a/src/tools/miri/tests/fail/function_calls/exported_symbol_shim_clashing.stderr +++ b/src/tools/miri/tests/fail/function_calls/exported_symbol_shim_clashing.stderr @@ -7,11 +7,8 @@ LL | malloc(0); help: the `malloc` symbol is defined here --> tests/fail/function_calls/exported_symbol_shim_clashing.rs:LL:CC | -LL | / extern "C" fn malloc(_: usize) -> *mut std::ffi::c_void { -LL | | -LL | | unreachable!() -LL | | } - | |_^ +LL | extern "C" fn malloc(_: usize) -> *mut std::ffi::c_void { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace diff --git a/src/tools/miri/tests/pass/extern_static.rs b/src/tools/miri/tests/pass/extern_static.rs new file mode 100644 index 0000000000000..70b8ff304c086 --- /dev/null +++ b/src/tools/miri/tests/pass/extern_static.rs @@ -0,0 +1,83 @@ +#![feature(sync_unsafe_cell)] + +use std::cell::SyncUnsafeCell; + +#[no_mangle] +static FOO: u8 = 42; + +#[export_name = "BAR_EXPORTED"] +static BAR_LOCAL_NAME: u16 = 1000; + +#[no_mangle] +static mut MUTABLE_STATIC: i32 = -1; + +#[export_name = "MY_LINK_NAME"] +static RUST_SYMBOL: u32 = 7; + +#[no_mangle] +static FOO_U32: u32 = 42; + +#[no_mangle] +static INTERIOR_MUT: SyncUnsafeCell = SyncUnsafeCell::new(42); + +fn increase_mutable_static_by_original_def(add_val: i32) { + unsafe { + let new_val = (&raw mut MUTABLE_STATIC).read() + add_val; + (&raw mut MUTABLE_STATIC).write(new_val); + } +} + +fn main() { + // The loop ensures we hit both the uncached and cached case. + for _ in 0..3 { + extern "Rust" { + static FOO: u8; + } + + assert_eq!(unsafe { (&raw const FOO).read() }, 42); + + extern "C" { + static BAR_EXPORTED: u16; + } + + assert_eq!(unsafe { (&raw const BAR_EXPORTED).read() }, 1000); + + extern "C" { + #[link_name = "MY_LINK_NAME"] + static EXTERN_STATIC: u32; + } + + assert_eq!(unsafe { (&raw const EXTERN_STATIC).read() }, 7); + + // Ensure that SyncUnsafeCell and `static mut` are interchangable. + extern "C" { + #[link_name = "INTERIOR_MUT"] + static mut INTERIOR_MUT_AS_MUTABLE_STATIC: i32; + #[link_name = "MUTABLE_STATIC"] + static MUTABLE_STATIC_AS_INTERIOR_MUT: SyncUnsafeCell; + } + unsafe { + (&raw mut INTERIOR_MUT_AS_MUTABLE_STATIC).write(7); + MUTABLE_STATIC_AS_INTERIOR_MUT.get().write(3); + } + } + + extern "Rust" { + static mut MUTABLE_STATIC: i32; + } + + // Check what happens if we mix accesses via the two aliases: the original + // definition at the top of the file, and the extern declaration just above. + unsafe { + assert_eq!((&raw const MUTABLE_STATIC).read(), 3); + (&raw mut MUTABLE_STATIC).write(32); + increase_mutable_static_by_original_def(10); + assert_eq!((&raw const MUTABLE_STATIC).read(), 42); + } + + extern "Rust" { + static FOO_U32: i32; + } + // This is like a transmute between raw pointers, so not UB. + assert_eq!(unsafe { (&raw const FOO_U32).read() }, 42i32); +} From a44d862247615198340c4da8fb06f824e75ed2c4 Mon Sep 17 00:00:00 2001 From: The Miri Cronjob Bot Date: Thu, 6 Aug 2026 05:27:12 +0000 Subject: [PATCH 41/42] Prepare for merging from rust-lang/rust This updates the rust-version file to f73951df0a5566d94d13b7954acd9f4ab1fa3734. --- src/tools/miri/rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index f29c624515673..8ab1fcaae5225 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -7218ebe93668f51a94a572b690c433dfdbdc2c3d +f73951df0a5566d94d13b7954acd9f4ab1fa3734 From dec94938eb6a69dd5437f68a02cd54c4626da132 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Thu, 6 Aug 2026 14:04:50 +0300 Subject: [PATCH 42/42] [Priroda] CI: add clippy check for priroda --- src/tools/miri/.github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/miri/.github/workflows/ci.yml b/src/tools/miri/.github/workflows/ci.yml index 5ede5327b625e..8b7935fb994a2 100644 --- a/src/tools/miri/.github/workflows/ci.yml +++ b/src/tools/miri/.github/workflows/ci.yml @@ -171,6 +171,9 @@ jobs: - name: build Priroda working-directory: priroda run: cargo build --locked + - name: clippy Priroda + working-directory: priroda + run: cargo clippy --all-targets --locked -- -D warnings - name: test Priroda working-directory: priroda run: |