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: | 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/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/`: diff --git a/src/tools/miri/priroda/src/debugger.rs b/src/tools/miri/priroda/src/debugger.rs new file mode 100644 index 0000000000000..b2b7c8779709a --- /dev/null +++ b/src/tools/miri/priroda/src/debugger.rs @@ -0,0 +1,856 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::Range; +use std::path::PathBuf; + +use miri::Immediate::Uninit; +use miri::*; +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 { + // 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, +} + +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. + /// + /// `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, +} + +/// 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) + } + /// 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()) + } + + /// Continue execution until reaching a breakpoint or propagating termination. + pub(super) fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { + self.resume(ResumeMode::Continue) + } + + 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. + // 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 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 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); + } + } + } + + _ => {} + } + } + + 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 + // 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 { + 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()); + + Some(SourceLocation { span, line: loc.line, column: loc.col_display + 1 }) + } + + 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. + pub(super) 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 + } +} + +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..6e48510cadc5c --- /dev/null +++ b/src/tools/miri/priroda/src/frontend/dap.rs @@ -0,0 +1,708 @@ +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::{ + ContinueResponse, ScopesResponse, SetBreakpointsResponse, StackTraceResponse, ThreadsResponse, + VariablesResponse, +}; +use emmy_dap_types::prelude::types::{ + 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}; + +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; + +enum HandlerResponse { + Success(ResponseBody), + Error(String), +} + +struct HandlerSuccess { + response: HandlerResponse, + state: Option, + events: Vec, + outcome: HandlerOutcome, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum HandlerOutcome { + Continue, + Exit, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DapState { + Fresh, + Initialized, + Launched, + Stopped, + Terminated, +} + +enum ExecutionOutcome { + Stopped(StepResult), + Terminated { code: i32 }, + Failed(String), +} + +/// 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> { + if let Err(err) = DapSession::stdio().run_requests(session) { + 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, + state: DapState, +} + +impl DapSession { + fn stdio() -> Self { + Self { + server: Server::new( + BufReader::new(io::stdin().lock()), + BufWriter::new(io::stdout().lock()), + ), + state: DapState::Fresh, + } + } + + fn run_requests<'tcx>( + &mut self, + session: &mut PrirodaContext<'tcx>, + ) -> Result<(), ServerError> { + loop { + let request = match self.server.poll_request() { + Ok(Some(request)) => request, + Ok(None) => return Ok(()), + Err(err) => return 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(msg) => { + self.server.respond(request.error(msg))?; + } + } + } + } + + fn dispatch_request<'tcx>( + &self, + request: &Request, + session: &mut PrirodaContext<'tcx>, + ) -> Result { + if self.state == DapState::Fresh && !matches!(&request.command, Command::Initialize(_)) { + return Err("initialize must be sent first"); + } + + 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(_) + | 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(_) => self.handle_unsupported_request(&request.command), + } + } + + /// FIXME: connect launch arguments to Priroda's session model. + fn handle_launch(&self) -> Result { + self.require_state(DapState::Initialized)?; + + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Launch), + state: Some(DapState::Launched), + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) + } + + fn handle_scopes<'tcx>( + &self, + frame_id: i64, + session: &PrirodaContext<'tcx>, + ) -> Result { + self.require_stopped()?; + Self::require_frame_id(frame_id)?; + + 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), + }; + 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>( + &self, + variables_reference: i64, + session: &PrirodaContext<'tcx>, + ) -> 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() + } else { + Vec::new() + }; + + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::Variables(VariablesResponse { + variables, + })), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) + } + + fn handle_configuration_done<'tcx>( + &self, + session: &mut PrirodaContext<'tcx>, + ) -> Result { + self.require_state(DapState::Launched)?; + + match Self::execution_outcome(session.stop_at_first_user_location()) { + 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 } => + 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) => + 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(&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>( + &self, + thread_id: i64, + session: &PrirodaContext<'tcx>, + ) -> Result { + self.require_stopped()?; + Self::require_thread_id(thread_id)?; + + 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: Some(0), + 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")); + 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(&self) -> Result { + if self.state != DapState::Fresh { + return Err("initialize may only be sent once"); + } + + 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>( + &self, + body: ResponseBody, + thread_id: i64, + session: &mut PrirodaContext<'tcx>, + ) -> Result { + self.require_stopped()?; + Self::require_thread_id(thread_id)?; + + match Self::execution_outcome(session.step()) { + ExecutionOutcome::Stopped(result) => + 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 } => + 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) => + Ok(HandlerSuccess { + response: HandlerResponse::Error(message), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }), + } + } + + fn handle_continue<'tcx>( + &self, + thread_id: i64, + session: &mut PrirodaContext<'tcx>, + ) -> 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) => + 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 } => + 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) => + Ok(HandlerSuccess { + response: HandlerResponse::Error(message), + state: Some(DapState::Terminated), + events: vec![Event::Terminated(None)], + outcome: HandlerOutcome::Exit, + }), + } + } + + fn handle_set_breakpoints<'tcx>( + &self, + args: &SetBreakpointsArguments, + session: &mut PrirodaContext<'tcx>, + ) -> Result { + self.reject_after_termination()?; + + let Some(ref path_str) = args.source.path else { + return Err( + "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 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, + }); + } + } + + Ok(HandlerSuccess { + response: HandlerResponse::Success(ResponseBody::SetBreakpoints( + SetBreakpointsResponse { breakpoints }, + )), + state: None, + events: Vec::new(), + outcome: HandlerOutcome::Continue, + }) + } + + 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( + &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> { + if self.state == DapState::Terminated { + return Err("request received after termination"); + } + Ok(()) + } + + fn require_state(&self, expected: DapState) -> Result<(), &'static str> { + if self.state != expected { + return Err(match expected { + DapState::Initialized => "launch requires initialize", + DapState::Launched => "configurationDone requires launch", + _ => "invalid session state for request", + }); + } + Ok(()) + } + + fn require_stopped(&self) -> Result<(), &'static str> { + if self.state != DapState::Stopped { + return Err("request requires a stopped frame"); + } + Ok(()) + } + + fn require_thread_id(thread_id: i64) -> Result<(), &'static str> { + if thread_id != THREAD_ID { + return Err("unknown threadId"); + } + Ok(()) + } + + fn require_frame_id(frame_id: i64) -> Result<(), &'static str> { + if frame_id != STACK_FRAME_ID { + return Err("unknown frameId"); + } + Ok(()) + } + + fn require_variables_reference(variables_reference: i64) -> Result<(), &'static str> { + if variables_reference != LOCALS_VARIABLES_REFERENCE { + return Err("unknown variablesReference"); + } + 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 stopped_event_body(reason: StoppedEventReason) -> StoppedEventBody { + 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 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", + 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", + 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", + } + } + + 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/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 bc0dacc589a79..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 miri::{interpret, *}; -use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; +use debugger::PrirodaContext; +use miri::*; 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") @@ -46,6 +37,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 +47,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 +105,10 @@ 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 => frontend::Cli {}.run_cli_loop(&mut session), + Frontend::Dap => frontend::Dap {}.run_dap_loop(&mut session), + }; match result.report_err() { Ok(()) => {} @@ -110,962 +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!("", 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 - } -} - -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) { - 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(()); - } - } - } else { - println!("no command"); - } - - io::stdout().flush().unwrap(); - } - } - - 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(&self, 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)) - } -} diff --git a/src/tools/miri/priroda/tests/cli.rs b/src/tools/miri/priroda/tests/cli.rs index 3b596fbf91f26..2bf7f22bd1d98 100644 --- a/src/tools/miri/priroda/tests/cli.rs +++ b/src/tools/miri/priroda/tests/cli.rs @@ -33,11 +33,20 @@ 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(); + // 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()), (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()), + (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.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..4f6f29a60dbd7 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize.stdout @@ -0,0 +1,5 @@ +Content-Length: {CONTENT_LENGTH} + +{"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.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..7ba36709bd123 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch.stdout @@ -0,0 +1,7 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {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.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..121232f9aa271 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout @@ -0,0 +1,11 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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.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..4a4df53ea5889 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout @@ -0,0 +1,11 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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.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..796935374a8eb --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout @@ -0,0 +1,13 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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.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..7ad4e38819f8f --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_non_initialize_first.stdout @@ -0,0 +1,3 @@ +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.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..abc6e1cf7d694 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout @@ -0,0 +1,17 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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 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} + +{"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..6baf6351f6a7b --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_rejects_wrong_ids.stdout @@ -0,0 +1,25 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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":"unknown threadId","error":null}Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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.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..4cc848bc88369 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables.stdout @@ -0,0 +1,17 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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":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,"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.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..558af9b383840 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_scopes_variables_next.stdout @@ -0,0 +1,31 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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":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,"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} + +{"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: {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,"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} + +{"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.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..1056d39e468b1 --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_stack_trace.stdout @@ -0,0 +1,15 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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":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.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..56702d4adc22e --- /dev/null +++ b/src/tools/miri/priroda/tests/ui/dap_threads.stdout @@ -0,0 +1,13 @@ +Content-Length: {CONTENT_LENGTH} + +{"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: {CONTENT_LENGTH} + +{"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: {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":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]},"error":null} \ No newline at end of file diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version index 2f35beeceda8c..8ab1fcaae5225 100644 --- a/src/tools/miri/rust-version +++ b/src/tools/miri/rust-version @@ -1 +1 @@ -73dc9167f1cd099e525c9ade2e068d1907b78564 +f73951df0a5566d94d13b7954acd9f4ab1fa3734 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", 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/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/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..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)); } @@ -74,8 +74,8 @@ 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| { - if let Some(body) = this.lookup_exported_symbol(link_name)? { + res.jump_to_next_block(this, &dest.clone().into(), ret, Some(unwind), |this| { + 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/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/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/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/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(()) 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-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; 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); +}