diff --git a/priroda/Cargo.lock b/priroda/Cargo.lock index 7d46f75d2f..48ba54ef8e 100644 --- a/priroda/Cargo.lock +++ b/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/priroda/Cargo.toml b/priroda/Cargo.toml index 88e6565344..ff299bae2a 100644 --- a/priroda/Cargo.toml +++ b/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/priroda/README.md b/priroda/README.md index a8c25bf279..6283bc28bb 100644 --- a/priroda/README.md +++ b/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/priroda/src/debugger.rs b/priroda/src/debugger.rs new file mode 100644 index 0000000000..b2b7c87797 --- /dev/null +++ b/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/priroda/src/frontend/cli.rs b/priroda/src/frontend/cli.rs new file mode 100644 index 0000000000..e4e92351a9 --- /dev/null +++ b/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/priroda/src/frontend/dap.rs b/priroda/src/frontend/dap.rs new file mode 100644 index 0000000000..6e48510cad --- /dev/null +++ b/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/priroda/src/frontend/mod.rs b/priroda/src/frontend/mod.rs new file mode 100644 index 0000000000..8d2f57fb67 --- /dev/null +++ b/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/priroda/src/main.rs b/priroda/src/main.rs index 4978b205b8..9b0efdf9fa 100644 --- a/priroda/src/main.rs +++ b/priroda/src/main.rs @@ -15,26 +15,17 @@ extern crate rustc_session; extern crate rustc_span; extern crate rustc_type_ir; -use std::collections::{HashMap, HashSet}; -use std::io::{self, Write}; -use std::num::NonZeroU64; -use std::ops::Range; -use std::path::PathBuf; +mod debugger; +mod frontend; -use miri::Immediate::Uninit; +use debugger::PrirodaContext; use miri::*; -use rustc_abi::{FIRST_VARIANT, FieldIdx, Size}; use rustc_driver::Compilation; use rustc_hir::attrs::CrateType; -use rustc_hir::def::CtorKind; use rustc_interface::interface; -use rustc_middle::mir::interpret::AllocId; -use rustc_middle::mir::{self, Local, ProjectionElem, VarDebugInfoContents, VarDebugInfoFragment}; -use rustc_middle::ty::{self, TyCtxt, TyKind}; +use rustc_middle::ty::TyCtxt; use rustc_session::EarlyDiagCtxt; use rustc_session::config::ErrorOutputType; -use rustc_span::source_map::SourceMap; -use rustc_span::{Span, Symbol}; fn find_sysroot() -> String { std::env::var("MIRI_SYSROOT") @@ -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,960 +137,3 @@ fn create_ecx<'tcx>(tcx: TyCtxt<'tcx>) -> MiriInterpCx<'tcx> { // FIXME: report interpreter initialization failures instead of panicking. miri::create_ecx(tcx, entry_id, entry_type, &config, None).unwrap() } - -/// Structured source information for frontends. -struct SourceLocation { - // storing `span` to use it lazily to compute path. - span: Span, - line: usize, -} - -impl SourceLocation { - fn local_path(&self, source_map: &SourceMap) -> Option { - let loc = source_map.lookup_char_pos(self.span.lo()); - loc.file.name.clone().into_local_path().map(normalize_path) - } -} - -/// Source-level breakpoints indexed by normalized path, then line. -type BreakpointTable = HashMap>; - -/// Owns one interpreter session and its debugger state. -/// -/// Frontend rendering should eventually live outside this type. -struct PrirodaContext<'tcx> { - ecx: MiriInterpCx<'tcx>, - breakpoints: BreakpointTable, - current_location: Option, - last_location: Option, -} - -enum StorageProj { - Field(usize), - Deref, - Downcast(Symbol), - Variant(usize), - Unsupported(String), -} - -impl StorageProj { - fn render(&self) -> String { - match self { - StorageProj::Field(field_idx) => format!(".{field_idx}"), - StorageProj::Deref => format!(".*"), - StorageProj::Downcast(name) => format!(" as {name}"), - StorageProj::Variant(variant_idx) => format!(" as variant#{variant_idx}"), - StorageProj::Unsupported(unsop) => format!("."), - } - } -} - -struct LocalDesc { - /// Source variable name from `VarDebugInfo`, if this row has one. - source_name: Option, - - /// Source-side projection from `VarDebugInfo::composite`, e.g. `.field` in source fragment `x.field`. - source_projection: Option>, - - /// MIR storage local that backs this description, if any. - local: Option, - - /// rendered/debug MIR place projection for now - storage_projection: Vec, - - /// Display-rendered type for this description. - ty: String, - - /// Run-time state for now; will be expanded later - value: String, -} - -/// Controls when execution returns to the frontend. -enum ResumeMode { - /// Stop at the next visible MIR instruction. - MirInstruction, - /// Stop at the next source line - /// - /// Take `Option` because some cases current state has no mapped to source code location - SourceLine(Option<(PathBuf, usize)>), - /// Continue until reaching a breakpoint. - Continue, -} - -/// Describes whether the current MIR instruction should be shown to the user. -enum InstructionVisibility { - NoInstruction, - Hidden, - Visible, -} - -/// Describes why execution stopped and returned control to the frontend. -enum StepResult { - Step, - Breakpoint, -} - -fn normalize_path(path: PathBuf) -> PathBuf { - path.canonicalize().unwrap_or(path) -} - -impl<'tcx> PrirodaContext<'tcx> { - fn new(ecx: MiriInterpCx<'tcx>) -> Self { - Self { ecx, breakpoints: HashMap::new(), current_location: None, last_location: None } - } - - fn local_path(&self, location: &SourceLocation) -> Option { - let source_map = self.ecx.tcx.sess.source_map(); - location.local_path(source_map) - } - - fn current_source_position(&self) -> Option<(PathBuf, usize)> { - let location = self.current_location.as_ref()?; - Some((self.local_path(location)?, location.line)) - } - - // Used to treat `continue` like a source-level step for breakpoint checks: - // several MIR locations can point at one source line, but they should only - // report that source breakpoint once. - fn last_source_position(&self) -> Option<(PathBuf, usize)> { - let location = self.last_location.as_ref()?; - Some((self.local_path(location)?, location.line)) - } - - /// Step to the next visible MIR instruction. - fn stepi(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::MirInstruction) - } - fn step(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::SourceLine(self.current_source_position())) - } - - /// Continue execution until reaching a breakpoint or propagating termination. - fn continue_execution(&mut self) -> InterpResult<'tcx, StepResult> { - self.resume(ResumeMode::Continue) - } - - fn set_breakpoint(&mut self, path: PathBuf, line: usize) -> BreakpointSetResult { - // FIXME: validate breakpoints here so every frontend gets the same behavior. - // Reject empty paths, missing files, directories, and line 0. Decide whether - // out-of-range lines should be rejected or kept as pending breakpoints. - // Report duplicate registrations separately. - - let path = normalize_path(path); - match self.breakpoints.entry(path.clone()).or_default().insert(line) { - true => BreakpointSetResult::Added(path, line), - false => BreakpointSetResult::Duplicate, - } - } - - /// Advance execution until the selected resume mode reaches a stopping point. - fn resume(&mut self, mode: ResumeMode) -> InterpResult<'tcx, StepResult> { - loop { - self.advance()?; - - // An explicit breakpoint should stop execution even when the current - // MIR instruction would normally be hidden during manual stepping. - if self.is_at_breakpoint() { - return interp_ok(StepResult::Breakpoint); - } - - match mode { - ResumeMode::MirInstruction - if matches!( - self.current_instruction_visibility(), - InstructionVisibility::Visible - ) => - { - return interp_ok(StepResult::Step); - } - - ResumeMode::SourceLine(ref prev_location) => { - match (prev_location, &self.current_location) { - // We started from an unmapped source location. Stop at the first mapped source location we can show to the user. - (None, Some(_)) => return interp_ok(StepResult::Step), - - (Some((prev_path, prev_line)), Some(current_location)) => { - if let Some(current_path) = self.local_path(current_location) { - // A source step stops when the visible source position changes to a different file or line. - if *prev_path != current_path || *prev_line != current_location.line - { - return interp_ok(StepResult::Step); - } - } - } - - _ => {} - } - } - - ResumeMode::MirInstruction | ResumeMode::Continue => {} - } - } - } - - /// Advance Miri by one interpreter-loop transition. - fn advance(&mut self) -> InterpResult<'tcx> { - // FIXME: use a Miri-owned scheduler-aware debugger step API before - // claiming support for multi-threaded interpreted programs. - - // State inspection should happen only after a successful step. - self.ecx.step_current_thread()?; - self.last_location = self.current_location.take(); - self.current_location = self.resolve_current_location(); - interp_ok(()) - } - - fn current_instruction_visibility(&self) -> InstructionVisibility { - // If the active thread has no stack frame, there is no MIR instruction to show. - let Some(frame) = self.ecx.active_thread_stack().last() else { - return InstructionVisibility::NoInstruction; - }; - - // `Right(span)` means the frame has source context but no precise MIR program-counter location. - let Either::Left(location) = frame.current_loc() else { - return InstructionVisibility::NoInstruction; - }; - - let basic_block = &frame.body().basic_blocks[location.block]; - - // `statement_index == statements.len()` points at the block terminator. - // Terminators affect control flow, so they are always visible. - let Some(statement) = basic_block.statements.get(location.statement_index) else { - return InstructionVisibility::Visible; - }; - - // Hide bookkeeping-only MIR statements during manual stepping. - match statement.kind { - mir::StatementKind::StorageLive(_) - | mir::StatementKind::StorageDead(_) - | mir::StatementKind::Nop => InstructionVisibility::Hidden, - _ => InstructionVisibility::Visible, - } - } - - fn is_at_breakpoint(&self) -> bool { - let Some(bp) = self.current_breakpoint() else { - return false; - }; - - // If the previous interpreter step had the same source position, this - // is another MIR location for the breakpoint we just reported. - self.last_source_position().as_ref() != Some(&bp) - } - - fn current_breakpoint(&self) -> Option<(PathBuf, usize)> { - let (path, line) = self.current_source_position()?; - let lines = self.breakpoints.get(&path)?; - - if lines.contains(&line) { Some((path, line)) } else { None } - } - - fn resolve_current_location(&self) -> Option { - // FIXME: resolve macro-backed lines such as `println!` and `assert_eq!` - // through `span.source_callsite()` before matching breakpoints. - let span = self.ecx.machine.current_user_relevant_span(); - if span.is_dummy() { - return None; - } - - let source_map = self.ecx.tcx.sess.source_map(); - let loc = source_map.lookup_char_pos(span.lo()); - - Some(SourceLocation { span, line: loc.line }) - } - - fn run_command(&mut self, command: DebuggerCommand) -> InterpResult<'tcx, CommandResult> { - match command { - DebuggerCommand::StepI => self.stepi().map(CommandResult::ExecutionStopped), - DebuggerCommand::Step => self.step().map(CommandResult::ExecutionStopped), - DebuggerCommand::Continue => - self.continue_execution().map(CommandResult::ExecutionStopped), - DebuggerCommand::Breakpoint(path, line) => - interp_ok(CommandResult::BreakpointResult(self.set_breakpoint(path, line))), - DebuggerCommand::ListLocals => interp_ok(CommandResult::Locals(self.list_locals())), - DebuggerCommand::Print(local) => - interp_ok(CommandResult::SingleLocal(self.get_local(local))), - DebuggerCommand::Follow(alloc_id, offset) => - self.follow_alloc(alloc_id, offset).map(CommandResult::Memory), - DebuggerCommand::TerminateSession => interp_ok(CommandResult::TerminateSession), - } - } - - fn follow_alloc(&self, alloc_id: AllocId, offset: usize) -> InterpResult<'tcx, String> { - let alloc = self.ecx.get_alloc_raw(alloc_id)?; - if offset > alloc.len() { - return Err(miri::err_unsup_format!( - "allocation offset {offset} is outside {alloc_id}" - )) - .into(); - } - - let memory = self.render_alloc_bytes(alloc_id, offset..alloc.len())?; - interp_ok(format!("Allocation {alloc_id}+{offset}: {memory}")) - } - - fn get_local(&self, local: usize) -> Option { - let frame = self.ecx.active_thread_stack().last()?; - - self.make_mir_local_desc(frame, local) - } - - /// Returns structured descriptions for locals in the innermost stack frame. - /// - /// Starts from all MIR locals, then enriches them with source names from - /// `var_debug_info` when a debug entry maps directly to a whole local. - fn list_locals(&self) -> Vec { - let Some(frame) = self.ecx.active_thread_stack().last() else { - return Vec::new(); - }; - - self.build_local_descs(frame) - } - - /// Renders the current byte range of an indirect MIR value. - /// - /// Initialized bytes are shown in hexadecimal, uninitialized bytes as `??`, - /// and complete pointer-sized provenance as pointer markers. - fn render_mplace_bytes(&self, mplace: &MPlaceTy<'tcx>) -> InterpResult<'tcx, String> { - let size = match self.ecx.size_and_align_of_val(mplace)? { - Some((size, _)) => size, - None => { - // Extern types cannot currently be executed as by-value locals, - // so this path cannot yet be covered by a Priroda UI fixture. - // FIXME: Add coverage once Priroda supports printing dereferenced places. - return interp_ok("".to_string()); - } - }; - - let size = size.bytes_usize(); - if size == 0 { - return interp_ok("[]".to_string()); - } - - let (alloc_id, offset, _) = - self.ecx.ptr_get_alloc_id(mplace.ptr(), size.try_into().unwrap())?; - let offset = offset.bytes_usize(); - let range = offset..offset.strict_add(size); - - self.render_alloc_bytes(alloc_id, range) - } - - /// Render a raw allocation range without requiring a typed memory place. - /// - /// This is also used by the future-facing `follow` command, where we have a - /// pointer target but do not yet know the target's type or size. - fn render_alloc_bytes( - &self, - alloc_id: AllocId, - range: Range, - ) -> InterpResult<'tcx, String> { - let alloc = self.ecx.get_alloc_raw(alloc_id)?; - - let mut rendered = Vec::with_capacity(range.len()); - - let ptr_size = self.ecx.tcx.data_layout.pointer_size(); - - for chunk in alloc.init_mask().range_as_init_chunks(range.into()) { - let chunk_range = chunk.range(); - let chunk_range = chunk_range.start.bytes_usize()..chunk_range.end.bytes_usize(); - - if chunk.is_init() { - let ptr_size = ptr_size.bytes_usize(); - let mut cursor = chunk_range.start; - - while cursor < chunk_range.end { - // Full pointer provenance is rendered as a pointer marker. Bytewise - // provenance fragments are intentionally left as raw bytes here: they do - // not represent a complete pointer-sized value. - if let Some(prov) = alloc.provenance().get_ptr(Size::from_bytes(cursor)) - && cursor + ptr_size <= chunk_range.end - { - let bytes = alloc.inspect_with_uninit_and_ptr_outside_interpreter( - cursor..cursor + ptr_size, - ); - let offset = read_target_uint(self.ecx.tcx.data_layout.endian, bytes) - .map_err(|err| { - miri::err_unsup_format!("invalid pointer representation: {err}") - })?; - - let offset = Size::from_bytes(offset); - rendered.push(format!("{:?}", Pointer::new(Some(prov), offset))); - - cursor += ptr_size; - } else { - let byte = alloc - .inspect_with_uninit_and_ptr_outside_interpreter(cursor..cursor + 1)[0]; - - rendered.push(format!("{byte:02x}")); - cursor += 1; - } - } - } else { - rendered.extend(std::iter::repeat_n("__".to_string(), chunk_range.len())); - } - } - - interp_ok(format!("[{}]", rendered.join(" "))) - } - - /// Render an evaluated operand using Rust-source-shaped containers with raw leaves. - /// - /// The operand is produced from live interpreter state, usually via `local_to_op` - /// for a whole MIR local or `eval_place_to_op` for a projected debug-info place. - /// - /// This intentionally does not call user `Debug` / `Display`, and it does not - /// try to make every scalar leaf pretty yet. Unsupported cases and leaf values - /// fall back to `render_op`, preserving the old raw byte/provenance renderer. - /// - /// FIXME: teach the leaf renderer about simple Rust scalars (`bool`, integers, - /// chars, raw pointers/references) once the source-shaped container output is - /// stable enough to stop depending on byte dumps for every field. - /// - /// FIXME: decide how much dereferencing belongs in this renderer. References - /// currently stay as raw pointer leaves; following them may belong in the - /// existing `follow` command instead of automatic local rendering. - fn render_source_shaped_op(&self, op: OpTy<'tcx>) -> String { - self.render_source_shaped_op_inner(op, 0) - } - - /// Recursive worker for `render_source_shaped_op`. - /// - /// The depth limit keeps cyclic/reference-heavy values from making debugger - /// output explode once more container kinds are added. At the limit, the raw - /// renderer remains the ground truth. - /// - /// FIXME: replace this fixed recursion limit with a value-size/output-budget - /// policy so large acyclic values and deeply nested values degrade more - /// predictably. - fn render_source_shaped_op_inner(&self, op: OpTy<'tcx>, depth: usize) -> String { - const MAX_SOURCE_SHAPE_DEPTH: usize = 8; - - if depth >= MAX_SOURCE_SHAPE_DEPTH { - return self.render_op(op); - } - - match op.layout.ty.kind() { - // Empty enums have no active variant to format. Unions do not record - // which field is currently active, so choosing one would be misleading. - // - // FIXME: support unions only with an explicit user-selected field or - // another source of active-field information. Guessing from layout - // bytes would make debugger output look more certain than it is. - ty::Adt(def, _) if def.variants().is_empty() || def.is_union() => self.render_op(op), - - ty::Adt(def, _) => { - // Enums need their runtime discriminant and a downcasted layout - // view before fields can be projected. Structs use their sole - // variant directly. Keep the display name tied to the same choice. - let (variant_idx, down, name) = if def.is_enum() { - let variant_idx = match self.ecx.read_discriminant(&op).discard_err() { - Some(variant_idx) => variant_idx, - // FIXME: expose this as an explicit render error when - // Priroda grows structured value states. Falling back to - // bytes keeps today's UI usable but hides why the enum - // could not be source-shaped. - None => return self.render_op(op), - }; - let down = match self.ecx.project_downcast(&op, variant_idx).discard_err() { - Some(down) => down, - // FIXME: distinguish invalid/uninitialized discriminants - // from projection bugs in the rendered output once locals - // can carry structured diagnostics. - None => return self.render_op(op), - }; - let variant_def = &def.variants()[variant_idx]; - ( - variant_idx, - down, - format!("{}::{}", self.ecx.tcx.item_name(def.did()), variant_def.name), - ) - } else { - let variant_idx = FIRST_VARIANT; - let variant_def = &def.variants()[variant_idx]; - (variant_idx, op.clone(), variant_def.name.to_string()) - }; - - let variant_def = &def.variants()[variant_idx]; - - let mut fields = Vec::with_capacity(variant_def.fields.len()); - for i in 0..variant_def.fields.len() { - let field_idx = FieldIdx::from_usize(i); - // `project_field` avoids manual offset math and works for both - // immediate and memory-backed operands through `Projectable`. - let field_op = match self.ecx.project_field(&down, field_idx).discard_err() { - Some(field_op) => field_op, - // FIXME: preserve the successfully rendered fields and - // mark only this field as unavailable once the value model - // can represent partial render failures. - None => return self.render_op(op), - }; - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); - } - - // Match Rust constructor spelling: - // - `Const`: unit structs/variants, e.g. `UnitStruct`, `Enum::Unit` - // - `Fn`: tuple structs/variants, e.g. `Pair(a, b)` or `EmptyTuple()` - // - `None`: braced structs/variants, including the empty `{}` case - match variant_def.ctor_kind() { - Some(CtorKind::Const) => name, - Some(CtorKind::Fn) => format!("{name}({})", fields.join(", ")), - None if fields.is_empty() => format!("{name} {{}}"), - None => { - let fields = variant_def - .fields - .iter() - .zip(fields) - .map(|(field_def, value)| format!("{}: {value}", field_def.name)) - .collect::>() - .join(", "); - format!("{name} {{ {fields} }}") - } - } - } - - ty::Tuple(args) => { - let mut fields = Vec::with_capacity(args.len()); - for i in 0..args.len() { - // Tuples have no field names in source, so preserve their - // source field order and render children positionally. - let field_op = - match self.ecx.project_field(&op, FieldIdx::from_usize(i)).discard_err() { - Some(field_op) => field_op, - // FIXME: render tuple fields independently so one - // projection failure does not throw away the whole - // source-shaped tuple. - None => return self.render_op(op), - }; - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)); - } - - if fields.len() == 1 { - format!("({},)", fields[0]) - } else { - format!("({})", fields.join(", ")) - } - } - - ty::Array(_, _) | ty::Slice(_) => { - // `project_array_fields` uses the dynamic length for slices. That - // avoids the classic mistake of treating slice layout as a fixed - // zero-length array. - let mut iter = match self.ecx.project_array_fields(&op).discard_err() { - Some(iter) => iter, - // FIXME: when slice metadata is invalid, show that as a slice - // length problem instead of silently falling back to raw bytes. - None => return self.render_op(op), - }; - - let mut fields = Vec::new(); - // FIXME: add an output budget/truncation policy before rendering - // very large arrays or slices in full. - loop { - match iter.next(&self.ecx).discard_err() { - Some(Some((_idx, field_op))) => - fields.push(self.render_source_shaped_op_inner(field_op, depth + 1)), - Some(None) => break, - // FIXME: keep already-rendered elements and mark the - // failed index once partial render errors are supported. - None => return self.render_op(op), - } - } - - format!("[{}]", fields.join(", ")) - } - - // FIXME: consider source-shaped special cases for strings, closures, - // generators/coroutines, trait objects, and SIMD/vector-like types. - // Until then these stay on the raw renderer path. - _ => self.render_op(op), - } - } - - /// Render an evaluated operand using the same raw representation for - /// whole locals and projected MIR places. - fn render_op(&self, op: OpTy<'tcx>) -> String { - match op.as_mplace_or_imm() { - Either::Right(imm) => format!("{imm}"), - - Either::Left(mplace) => - match self.render_mplace_bytes(&mplace).report_err() { - Ok(bytes) => bytes, - Err(err) => format!("", err.to_string()), - }, - } - } - - /// Render the source-side path from composite debug info, such as `.field`. - fn render_source_projection( - fragment: Option<&VarDebugInfoFragment<'tcx>>, - ) -> Option> { - let VarDebugInfoFragment { ty, projection } = fragment?; - - // Walk the source-side projection from the original - // composite variable type. Each `Field` element stores the - // resulting field type, so resolve the field name from the - // current base type before advancing to `field_ty`. - let mut projection_ty = ty; - - Some( - projection - .iter() - .map(|elem| { - match elem { - ProjectionElem::Field(field_idx, field_ty) => { - let rendered = match projection_ty.kind() { - TyKind::Adt(adt_def, _args) if adt_def.is_struct() => { - let variant = adt_def.non_enum_variant(); - let field = &variant.fields[*field_idx]; - Symbol::intern(&format!(".{}", field.name)) - } - - TyKind::Tuple(_) => - Symbol::intern(&format!(".{}", field_idx.index())), - - _ => Symbol::intern("."), - }; - - projection_ty = field_ty; - - rendered - } - // `VarDebugInfoFragment::projection` is expected to be - // field-only. If that ever changes, keep the unexpected - // segment visible instead of silently rendering a - // misleading source path. - other => Symbol::intern(&format!(".")), - } - }) - .collect(), - ) - } - - /// Render the MIR storage-side path that backs a debug-info local. - fn render_storage_projection(projection: &[mir::PlaceElem<'tcx>]) -> Vec { - projection - .iter() - .map(|projection_elem| { - match projection_elem { - ProjectionElem::Field(field_idx, _) => StorageProj::Field(field_idx.index()), - ProjectionElem::Deref => StorageProj::Deref, - ProjectionElem::Downcast(Some(name), _) => StorageProj::Downcast(*name), - ProjectionElem::Downcast(None, variant_idx) => - StorageProj::Variant(variant_idx.index()), - other => StorageProj::Unsupported(format!("{other:?}")), - } - }) - .collect() - } - - /// Builds the baseline debugger row for one MIR local without scanning debug info. - fn make_mir_local_desc( - &self, - frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, - local: usize, - ) -> Option { - let local = mir::Local::from_usize(local); - let local_decl = frame.body().local_decls.get(local)?; - - // Create LocalDesc for MIR local before processing debug info. - // Debug-info enrichment is layered on by build_local_descs. - let mut local_desc = LocalDesc { - source_name: None, - source_projection: None, - local: Some(local), - storage_projection: Vec::new(), - ty: local_decl.ty.to_string(), - value: "".to_string(), - }; - - match &frame.locals[local].as_mplace_or_imm() { - None => { - local_desc.value = "".to_string(); - } - Some(Either::Right(Uninit)) => local_desc.value = "".to_string(), - - Some(Either::Left(_) | Either::Right(_)) => { - let op = self - .ecx - .local_to_op(local, None) - .expect("this error can only occur in CTFE on generic code"); - local_desc.value = self.render_source_shaped_op(op); - } - }; - - Some(local_desc) - } - - fn build_local_descs( - &self, - frame: &Frame<'tcx, Provenance, FrameExtra<'tcx>>, - ) -> Vec { - let local_decls = &frame.body().local_decls; - - let mut local_descs: Vec = Vec::with_capacity(local_decls.len()); - - // Start with one baseline row for every MIR local, then layer debug info on top. - for (local_idx, _) in local_decls.iter_enumerated() { - local_descs.push(self.make_mir_local_desc(frame, local_idx.index()).unwrap()); - } - - // FIXME: Finish classifying `var_debug_info` by keeping the source path - // and MIR storage path separate: - // - // - source side: `var_debug_info.name` plus - // `var_debug_info.composite.projection` - // - storage side: `VarDebugInfoContents::Place(place).local` plus - // `place.projection` - // - // Already handled by the `place.as_local()` path below: - // - whole source variable -> whole MIR local: - // `composite = None`, `Place(_N)` with empty projection. - // - source fragment -> whole MIR local: - // `composite = Some(source_proj)`, `Place(_N)` with empty projection. - // - // Remaining cases to represent or explicitly defer: - // - whole source variable -> projected MIR storage: - // `composite = None`, `Place(_N.proj)`. - // - source fragment -> projected MIR storage: - // `composite = Some(source_proj)`, `Place(_N.storage_proj)`. - // - source variable/fragment -> constant: - // `Const(...)`, with no MIR local id. - // - optimized-out/debug-only/unsupported shapes: - // explicit deferred state, not silent discard. - // - // Final output should be produced by walking `Vec`, - // then append explicit deferred/debug-info-only rows where needed. - // Related: SROA can split a source local like `_slice: ExtraSlice` into - // field locals whose debug paths should be printed as `_slice._slice` - // and `_slice._extra`, not as two separate locals both named `_slice`. - - // Whole-place debug entries enrich the direct storage-local description. - // Projected places are evaluated from their original MIR Place and use - // the same raw renderer as ordinary locals. - for var_debug_info in &frame.body().var_debug_info { - if let VarDebugInfoContents::Place(place) = &var_debug_info.value { - if let Some(local_idx) = place.as_local() - && local_descs[local_idx.index()].source_name.is_none() - { - let local_idx = local_idx.index(); - local_descs[local_idx].source_projection = - Self::render_source_projection(var_debug_info.composite.as_deref()); - local_descs[local_idx].source_name = Some(var_debug_info.name); - } else if !place.projection.is_empty() { - let storage_projection = Self::render_storage_projection(place.projection); - let source_projection = - Self::render_source_projection(var_debug_info.composite.as_deref()); - let value = self - .ecx - .eval_place_to_op(*place, None) - .map(|op| self.render_source_shaped_op(op)) - .unwrap_or_else(|err| format!("", err.to_string())); - - local_descs.push(LocalDesc { - source_name: Some(var_debug_info.name), - source_projection, - local: Some(place.local), - storage_projection, - ty: place.ty(local_decls, self.ecx.tcx.tcx).ty.to_string(), - value, - }); - } - } - } - - local_descs - } -} - -enum DebuggerCommand { - StepI, - Step, - TerminateSession, - Continue, - Breakpoint(PathBuf, usize), - ListLocals, - Print(usize), - Follow(AllocId, usize), -} - -enum BreakpointSetResult { - Added(PathBuf, usize), - Duplicate, - // FIXME: add pending breakpoint support later if needed. -} - -enum CommandResult { - ExecutionStopped(StepResult), - BreakpointResult(BreakpointSetResult), - Locals(Vec), - SingleLocal(Option), - Memory(String), - // FIXME: distinguish terminating the debugger session from disconnecting a - // frontend and terminating the interpreted program once multiple frontends exist. - TerminateSession, -} - -struct Cli; - -impl Cli { - pub fn run_cli_loop<'tcx>(&self, session: &mut PrirodaContext<'tcx>) -> InterpResult<'tcx> { - loop { - print!("(priroda) "); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - let bytes_read = io::stdin().read_line(&mut input).unwrap(); - - if bytes_read == 0 { - println!("stdin closed, stopping"); - return interp_ok(()); - } - - if let Some(command) = self.parse_command(&input) { - 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/priroda/tests/cli.rs b/priroda/tests/cli.rs index 3b596fbf91..2bf7f22bd1 100644 --- a/priroda/tests/cli.rs +++ b/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/priroda/tests/ui/dap_initialize.rs b/priroda/tests/ui/dap_initialize.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_initialize.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_initialize.stdin b/priroda/tests/ui/dap_initialize.stdin new file mode 100644 index 0000000000..873743fad3 --- /dev/null +++ b/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/priroda/tests/ui/dap_initialize.stdout b/priroda/tests/ui/dap_initialize.stdout new file mode 100644 index 0000000000..4f6f29a60d --- /dev/null +++ b/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/priroda/tests/ui/dap_initialize_launch.rs b/priroda/tests/ui/dap_initialize_launch.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_initialize_launch.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_initialize_launch.stdin b/priroda/tests/ui/dap_initialize_launch.stdin new file mode 100644 index 0000000000..ae4ee94ca0 --- /dev/null +++ b/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/priroda/tests/ui/dap_initialize_launch.stdout b/priroda/tests/ui/dap_initialize_launch.stdout new file mode 100644 index 0000000000..7ba36709bd --- /dev/null +++ b/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/priroda/tests/ui/dap_initialize_launch_configuration_done.rs b/priroda/tests/ui/dap_initialize_launch_configuration_done.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_initialize_launch_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin b/priroda/tests/ui/dap_initialize_launch_configuration_done.stdin new file mode 100644 index 0000000000..106ce5dac3 --- /dev/null +++ b/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/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout b/priroda/tests/ui/dap_initialize_launch_configuration_done.stdout new file mode 100644 index 0000000000..121232f9aa --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs b/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_rejects_configuration_done_before_launch.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin b/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdin new file mode 100644 index 0000000000..c1dedb5404 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout b/priroda/tests/ui/dap_rejects_configuration_done_before_launch.stdout new file mode 100644 index 0000000000..4a4df53ea5 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs b/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_rejects_next_before_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin b/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdin new file mode 100644 index 0000000000..a582d4adc7 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout b/priroda/tests/ui/dap_rejects_next_before_configuration_done.stdout new file mode 100644 index 0000000000..796935374a --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_non_initialize_first.rs b/priroda/tests/ui/dap_rejects_non_initialize_first.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_rejects_non_initialize_first.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_rejects_non_initialize_first.stdin b/priroda/tests/ui/dap_rejects_non_initialize_first.stdin new file mode 100644 index 0000000000..6b8fb8e084 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_non_initialize_first.stdout b/priroda/tests/ui/dap_rejects_non_initialize_first.stdout new file mode 100644 index 0000000000..7ad4e38819 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs b/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_rejects_repeated_configuration_done.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin b/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdin new file mode 100644 index 0000000000..d98039165e --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout b/priroda/tests/ui/dap_rejects_repeated_configuration_done.stdout new file mode 100644 index 0000000000..abc6e1cf7d --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_wrong_ids.rs b/priroda/tests/ui/dap_rejects_wrong_ids.rs new file mode 100644 index 0000000000..cd7ad8e0bb --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_wrong_ids.stdin b/priroda/tests/ui/dap_rejects_wrong_ids.stdin new file mode 100644 index 0000000000..a2a9dd1595 --- /dev/null +++ b/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/priroda/tests/ui/dap_rejects_wrong_ids.stdout b/priroda/tests/ui/dap_rejects_wrong_ids.stdout new file mode 100644 index 0000000000..6baf6351f6 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables.rs b/priroda/tests/ui/dap_scopes_variables.rs new file mode 100644 index 0000000000..081c3ce1d9 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables.stdin b/priroda/tests/ui/dap_scopes_variables.stdin new file mode 100644 index 0000000000..d1dd783eb9 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables.stdout b/priroda/tests/ui/dap_scopes_variables.stdout new file mode 100644 index 0000000000..4cc848bc88 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables_next.rs b/priroda/tests/ui/dap_scopes_variables_next.rs new file mode 100644 index 0000000000..081c3ce1d9 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables_next.stdin b/priroda/tests/ui/dap_scopes_variables_next.stdin new file mode 100644 index 0000000000..40da18a583 --- /dev/null +++ b/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/priroda/tests/ui/dap_scopes_variables_next.stdout b/priroda/tests/ui/dap_scopes_variables_next.stdout new file mode 100644 index 0000000000..558af9b383 --- /dev/null +++ b/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/priroda/tests/ui/dap_stack_trace.rs b/priroda/tests/ui/dap_stack_trace.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_stack_trace.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_stack_trace.stdin b/priroda/tests/ui/dap_stack_trace.stdin new file mode 100644 index 0000000000..1056beef57 --- /dev/null +++ b/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/priroda/tests/ui/dap_stack_trace.stdout b/priroda/tests/ui/dap_stack_trace.stdout new file mode 100644 index 0000000000..1056d39e46 --- /dev/null +++ b/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/priroda/tests/ui/dap_threads.rs b/priroda/tests/ui/dap_threads.rs new file mode 100644 index 0000000000..c1f1ed6f67 --- /dev/null +++ b/priroda/tests/ui/dap_threads.rs @@ -0,0 +1,3 @@ +//@ compile-flags: --dap + +fn main() {} diff --git a/priroda/tests/ui/dap_threads.stdin b/priroda/tests/ui/dap_threads.stdin new file mode 100644 index 0000000000..a17c6406c9 --- /dev/null +++ b/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/priroda/tests/ui/dap_threads.stdout b/priroda/tests/ui/dap_threads.stdout new file mode 100644 index 0000000000..56702d4adc --- /dev/null +++ b/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