blob: a4766e53314cbcbdc86779676d211af88ee7fc57 [file] [edit]
use std::collections::{HashMap, HashSet};
use std::ops::Range;
use std::path::PathBuf;
use miri::Immediate::Uninit;
use miri::{InterpErrorInfo, InterpErrorKind, TerminationInfo, *};
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<PathBuf> {
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<PathBuf, HashSet<usize>>;
/// 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<SourceLocation>,
last_location: Option<SourceLocation>,
// FIXME: add restart and other post-exit commands, similar to GDB and
// old Priroda, instead of only replaying the saved exit code.
exit_code: Option<i32>,
}
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!(".<unsupported:{unsop}>"),
}
}
}
pub(super) struct LocalDesc {
/// Source variable name from `VarDebugInfo`, if this row has one.
pub(super) source_name: Option<Symbol>,
/// Source-side projection from `VarDebugInfo::composite`, e.g. `.field` in source fragment `x.field`.
pub(super) source_projection: Option<Vec<Symbol>>,
/// MIR storage local that backs this description, if any.
pub(super) local: Option<Local>,
/// rendered/debug MIR place projection for now
pub(super) storage_projection: Vec<StorageProj>,
/// 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::<String>())
.unwrap_or_default()
}
pub(super) fn storage_projection_str(&self) -> String {
self.storage_projection.iter().map(StorageProj::render).collect::<String>()
}
}
/// Controls when execution returns to the frontend.
enum ResumeMode {
/// Stop at the next visible MIR instruction.
MirInstruction,
/// Step over the source position `start_position`, entered from a stack of
/// depth `start_stack_depth`.
///
/// Execution keeps going while it is deeper than `start_stack_depth` (i.e.
/// inside a call made from the stepped-over line), and stops once it is back
/// at that depth or shallower and the displayed source position has changed.
/// A `start_stack_depth` of `usize::MAX` means execution is never deeper,
/// turning this into a plain source step that also stops inside called functions.
StepOver { start_position: Option<(PathBuf, usize)>, start_stack_depth: usize },
/// Step out of the current user frame, stopping once execution returns to a
/// shallower user-frame depth.
StepOut { start_position: Option<(PathBuf, usize)>, start_user_frame_depth: 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,
}
impl ResumeMode {
fn skipped_breakpoint(&self) -> Option<&(PathBuf, usize)> {
match self {
ResumeMode::StepOver { start_position: Some(position), .. }
| ResumeMode::StepOut { start_position: Some(position), .. } => Some(position),
_ => None,
}
}
}
/// Describes why execution stopped and returned control to the frontend.
pub(super) enum StepResult {
Step,
Breakpoint,
Exception { message: String },
}
pub(super) enum ExecutionResult {
Stopped(StepResult),
ProgramExited { code: i32 },
Rejected { message: &'static str },
}
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,
exit_code: None,
}
}
pub(super) fn local_path(&self, location: &SourceLocation) -> Option<PathBuf> {
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))
}
fn already_finished(&self) -> Option<ExecutionResult> {
self.exit_code.map(|code| ExecutionResult::ProgramExited { code })
}
/// Step to the next visible MIR instruction.
fn stepi(&mut self) -> InterpResult<'tcx, ExecutionResult> {
if let Some(result) = self.already_finished() {
return interp_ok(result);
}
self.resume(ResumeMode::MirInstruction)
}
/// Step until the displayed source file or line changes.
///
/// This is the CLI source-level step; it shares its stepping semantics with
/// [`Self::step_in_source`].
pub(super) fn step(&mut self) -> InterpResult<'tcx, ExecutionResult> {
self.step_in_source()
}
/// Step into the next source location.
///
/// This can enter calls that have a distinct displayed source position,
/// while `next` uses [`Self::step_over_source`].
pub(super) fn step_in_source(&mut self) -> InterpResult<'tcx, ExecutionResult> {
if let Some(result) = self.already_finished() {
return interp_ok(result);
}
self.resume(ResumeMode::StepOver {
start_position: self.current_source_position(),
start_stack_depth: usize::MAX,
})
}
/// Step over the current source position, not stopping inside any call it makes.
///
/// Records the current source position and stack depth before advancing,
/// then keeps stepping until execution is back at that depth (or shallower)
/// and the displayed source position has changed.
pub(super) fn step_over_source(&mut self) -> InterpResult<'tcx, ExecutionResult> {
if let Some(result) = self.already_finished() {
return interp_ok(result);
}
let start_position = self.current_source_position();
let start_stack_depth = self.active_thread_stack_depth();
self.resume(ResumeMode::StepOver { start_position, start_stack_depth })
}
/// Number of frames on the active thread's stack.
fn active_thread_stack_depth(&self) -> usize {
self.ecx.active_thread_stack().len()
}
/// Step out of the current user frame.
///
/// Records the current user-frame depth and runs until execution reaches a
/// source location in a shallower user frame.
pub(super) fn step_out_source(&mut self) -> InterpResult<'tcx, ExecutionResult> {
if let Some(result) = self.already_finished() {
return interp_ok(result);
}
let start_user_frame_depth = self.active_user_frame_depth();
if start_user_frame_depth <= 1 {
return interp_ok(ExecutionResult::Rejected {
message: "stepOut is not meaningful in the outermost user frame",
});
}
let start_position = self.current_source_position();
self.resume(ResumeMode::StepOut { start_position, start_user_frame_depth })
}
/// Run until the initial editor-visible stop point.
pub(super) fn stop_at_first_user_location(&mut self) -> InterpResult<'tcx, ExecutionResult> {
self.resume(ResumeMode::FirstUserSourceLocation)
}
/// Return the active frame name while DAP still reports only one frame.
pub(super) fn current_frame_name(&self) -> Option<String> {
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, ExecutionResult> {
if let Some(result) = self.already_finished() {
return interp_ok(result);
}
self.resume(ResumeMode::Continue)
}
pub(super) fn finish_session(&mut self) -> InterpResult<'tcx, ()> {
interp_ok(())
}
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,
}
}
fn program_exit(err: &InterpErrorInfo<'tcx>) -> Option<i32> {
let InterpErrorKind::MachineStop(info) = err.kind() else {
return None;
};
// FIXME: Preserve `TerminationInfo::Exit::leak_check` and run Miri's
// leak/thread-leak diagnostics once Priroda grows a proper post-exit
// finalization path. For now, program exit only records the debuggee exit code.
let Some(TerminationInfo::Exit { code, .. }) = info.downcast_ref::<TerminationInfo>()
else {
return None;
};
Some(*code)
}
fn stop_at_exception(&mut self, err: InterpErrorInfo<'tcx>) -> StepResult {
let message = err.kind().to_string();
self.last_location = self.current_location.take();
self.current_location = self.resolve_current_location();
StepResult::Exception { message }
}
/// Advance execution until the selected resume mode reaches a stopping point.
fn resume(&mut self, mode: ResumeMode) -> InterpResult<'tcx, ExecutionResult> {
loop {
// Program exits are not debugger exceptions. Preserve all other
// interpreter errors as stopped debugger events.
if let Err(err) = self.advance().report_err() {
if let Some(code) = Self::program_exit(&err) {
self.exit_code = Some(code);
return interp_ok(ExecutionResult::ProgramExited { code });
}
return interp_ok(ExecutionResult::Stopped(self.stop_at_exception(err)));
}
// An explicit breakpoint should stop execution even when the current
// MIR instruction would normally be hidden during manual stepping.
if self.is_at_breakpoint(mode.skipped_breakpoint()) {
return interp_ok(ExecutionResult::Stopped(StepResult::Breakpoint));
}
match mode {
ResumeMode::MirInstruction
if matches!(
self.current_instruction_visibility(),
InstructionVisibility::Visible
) =>
{
return interp_ok(ExecutionResult::Stopped(StepResult::Step));
}
ResumeMode::StepOver { ref start_position, start_stack_depth } => {
// While deeper than where we started, we are inside a call
// made from the stepped-over line; keep going.
if self.active_thread_stack_depth() > start_stack_depth {
continue;
}
// Back at (or shallower than) the starting depth: stop once
// the displayed source position has changed.
match (start_position, &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(ExecutionResult::Stopped(StepResult::Step)),
(Some((start_path, start_line)), Some(current_location)) => {
// A source step stops when the displayed source
// position changes to a different file or line.
if let Some(current_path) = self.local_path(current_location)
&& (*start_path != current_path
|| *start_line != current_location.line)
{
// Return spans can point at a function header. Keep walking when
// that would move `next` backwards within the same frame.
if self.active_thread_stack_depth() == start_stack_depth
&& *start_path == current_path
&& current_location.line < *start_line
{
continue;
}
return interp_ok(ExecutionResult::Stopped(StepResult::Step));
}
}
_ => {}
}
}
ResumeMode::StepOut { start_user_frame_depth, .. }
if self.active_user_frame_depth() < start_user_frame_depth
&& self.current_location.is_some() =>
{
return interp_ok(ExecutionResult::Stopped(StepResult::Step));
}
ResumeMode::FirstUserSourceLocation
if self.current_location.is_some() && self.has_user_relevant_frame() =>
{
return interp_ok(ExecutionResult::Stopped(StepResult::Step));
}
ResumeMode::MirInstruction
| ResumeMode::FirstUserSourceLocation
| ResumeMode::StepOut { .. }
| ResumeMode::Continue => {}
}
}
}
fn has_user_relevant_frame(&self) -> bool {
self.active_user_frame_depth() > 0
}
fn active_user_frame_depth(&self) -> usize {
// 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()
.filter(|frame| frame.extra.user_relevance == u8::MAX)
.count()
}
/// 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, skipped_breakpoint: Option<&(PathBuf, usize)>) -> bool {
let Some(bp) = self.current_breakpoint() else {
return false;
};
if skipped_breakpoint == Some(&bp) {
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<SourceLocation> {
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::Execution),
DebuggerCommand::Step => self.step().map(CommandResult::Execution),
DebuggerCommand::Next => self.step_over_source().map(CommandResult::Execution),
DebuggerCommand::StepOut => self.step_out_source().map(CommandResult::Execution),
DebuggerCommand::Continue => self.continue_execution().map(CommandResult::Execution),
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 =>
self.finish_session().map(|()| 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<LocalDesc> {
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<LocalDesc> {
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 Some((size, _)) = self.ecx.size_and_align_of_val(mplace)? else {
// 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("<unsupported-unsized>".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<usize>,
) -> 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 Some(variant_idx) = self.ecx.read_discriminant(&op).discard_err() else {
// 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.
return self.render_op(op);
};
let Some(down) = self.ecx.project_downcast(&op, variant_idx).discard_err()
else {
// FIXME: distinguish invalid/uninitialized discriminants
// from projection bugs in the rendered output once locals
// can carry structured diagnostics.
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 Some(field_op) = self.ecx.project_field(&down, field_idx).discard_err()
else {
// FIXME: preserve the successfully rendered fields and
// mark only this field as unavailable once the value model
// can represent partial render failures.
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::<Vec<_>>()
.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 Some(field_op) =
self.ecx.project_field(&op, FieldIdx::from_usize(i)).discard_err()
else {
// FIXME: render tuple fields independently so one
// projection failure does not throw away the whole
// source-shaped tuple.
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 Some(mut iter) = self.ecx.project_array_fields(&op).discard_err() else {
// FIXME: when slice metadata is invalid, show that as a slice
// length problem instead of silently falling back to raw bytes.
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!("<error: {}>", err.to_string()),
},
}
}
/// Render the source-side path from composite debug info, such as `.field`.
fn render_source_projection(
fragment: Option<&VarDebugInfoFragment<'tcx>>,
) -> Option<Vec<Symbol>> {
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(".<unexpected>"),
};
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!(".<unsupported:{other:?}>")),
}
})
.collect(),
)
}
/// Render the MIR storage-side path that backs a debug-info local.
fn render_storage_projection(projection: &[mir::PlaceElem<'tcx>]) -> Vec<StorageProj> {
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<LocalDesc> {
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: "<unsupported>".to_string(),
};
match &frame.locals()[local].as_mplace_or_imm_ghost() {
None => {
local_desc.value = "<dead>".to_string();
}
Some(Either::Right(Uninit)) => local_desc.value = "<uninit>".to_string(),
Some(Either::Left(_) | Either::Right(_)) => {
// FIXME: This seems wrong, it ignore the frame.
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<LocalDesc> {
let local_decls = &frame.body().local_decls;
let mut local_descs: Vec<LocalDesc> = 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<LocalDesc>`,
// 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!("<error: {}>", 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,
Next,
StepOut,
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 {
Execution(ExecutionResult),
BreakpointResult(BreakpointSetResult),
Locals(Vec<LocalDesc>),
SingleLocal(Option<LocalDesc>),
Memory(String),
// FIXME: distinguish terminating the debugger session from disconnecting a
// frontend and terminating the interpreted program once multiple frontends exist.
TerminateSession,
}