diff --git a/Cargo.lock b/Cargo.lock index 6e1fa2a2d68..f776210b4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2837,6 +2837,7 @@ name = "sway-core" version = "0.3.3" dependencies = [ "derivative", + "dirs 3.0.2", "either", "fuel-asm", "fuel-pest", diff --git a/forc/src/cli/commands/addr2line.rs b/forc/src/cli/commands/addr2line.rs new file mode 100755 index 00000000000..fd0d68242f5 --- /dev/null +++ b/forc/src/cli/commands/addr2line.rs @@ -0,0 +1,139 @@ +use std::collections::VecDeque; +use std::fs::{self, File}; +use std::io::{self, prelude::*, BufReader}; +use std::path::{Path, PathBuf}; +use structopt::{self, StructOpt}; + +use annotate_snippets::{ + display_list::{DisplayList, FormatOptions}, + snippet::{AnnotationType, Slice, Snippet, SourceAnnotation}, +}; + +use sway_core::source_map::{LocationRange, SourceMap}; + +/// Show location and context of an opcode address in its source file +#[derive(Debug, StructOpt)] +pub(crate) struct Command { + /// Where to search for the project root + #[structopt(short = "s", long, default_value = ".")] + pub search_dir: PathBuf, + /// Source file mapping in JSON format + #[structopt(short = "g", long)] + pub sourcemap_path: PathBuf, + /// How many lines of context to show + #[structopt(short, long, default_value = "2")] + pub context: usize, + /// Opcode index + #[structopt(short = "i", long)] + pub opcode_index: usize, +} + +pub(crate) fn exec(command: Command) -> Result<(), String> { + let contents = fs::read(&command.sourcemap_path) + .map_err(|err| format!("{:?}: could not read: {:?}", command.sourcemap_path, err))?; + + let sm: SourceMap = serde_json::from_slice(&contents).map_err(|err| { + format!( + "{:?}: invalid source map json: {}", + command.sourcemap_path, err + ) + })?; + + if let Some((mut path, range)) = sm.addr_to_span(command.opcode_index) { + if path.is_relative() { + path = command.search_dir.join(path); + } + + let rr = read_range(&path, range, command.context) + .map_err(|err| format!("{:?}: could not read: {:?}", path, err))?; + + let path_str = format!("{:?}", path); + let snippet = Snippet { + title: None, + footer: vec![], + slices: vec![Slice { + source: &rr.source, + line_start: rr.source_start_line, + origin: Some(&path_str), + fold: false, + annotations: vec![SourceAnnotation { + label: "here", + annotation_type: AnnotationType::Note, + range: (rr.offset, rr.offset + rr.length), + }], + }], + opt: FormatOptions { + color: true, + ..Default::default() + }, + }; + println!("{}", DisplayList::from(snippet)); + + Ok(()) + } else { + Err("Address did not map to any source code location".to_owned()) + } +} + +struct ReadRange { + source: String, + source_start_byte: usize, + source_start_line: usize, + offset: usize, + length: usize, +} + +fn read_range>( + path: P, + range: LocationRange, + context_lines: usize, +) -> io::Result { + let file = File::open(&path)?; + let mut reader = BufReader::new(file); + let mut context_buffer = VecDeque::new(); + + let mut start_pos = None; + let mut position = 0; + for line_num in 0.. { + let mut buffer = String::new(); + let n = reader.read_line(&mut buffer)?; + context_buffer.push_back(buffer); + if start_pos.is_none() { + if position + n > range.start { + let cbl: usize = context_buffer.iter().map(|c| c.len()).sum(); + start_pos = Some((line_num, position, range.start - (position + n - cbl))); + } else if context_buffer.len() > context_lines { + let _ = context_buffer.pop_front(); + } + } else if context_buffer.len() > context_lines * 2 { + break; + } + + position += n; + } + + let source = context_buffer.make_contiguous().join(""); + let length = range.end - range.start; + + let (source_start_line, source_start_byte, offset) = start_pos.ok_or_else(|| { + io::Error::new( + io::ErrorKind::UnexpectedEof, + "Source file was modified, and the mapping is now out of range", + ) + })?; + + if offset + length > source.len() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Source file was modified, and the mapping is now out of range", + )); + } + + Ok(ReadRange { + source, + source_start_byte, + source_start_line, + offset, + length, + }) +} diff --git a/forc/src/cli/commands/mod.rs b/forc/src/cli/commands/mod.rs index 65370919d2b..3b6909e902e 100644 --- a/forc/src/cli/commands/mod.rs +++ b/forc/src/cli/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod addr2line; pub mod build; pub mod deploy; pub mod format; diff --git a/forc/src/cli/mod.rs b/forc/src/cli/mod.rs index 198419b3ebd..e4a9b69e290 100644 --- a/forc/src/cli/mod.rs +++ b/forc/src/cli/mod.rs @@ -2,9 +2,10 @@ use structopt::StructOpt; mod commands; use self::commands::{ - build, deploy, format, init, json_abi, lsp, parse_bytecode, run, test, update, + addr2line, build, deploy, format, init, json_abi, lsp, parse_bytecode, run, test, update, }; +use addr2line::Command as Addr2LineCommand; pub use build::Command as BuildCommand; pub use deploy::Command as DeployCommand; pub use format::Command as FormatCommand; @@ -26,6 +27,8 @@ struct Opt { #[derive(Debug, StructOpt)] enum Forc { + #[structopt(name = "addr2line")] + Addr2Line(Addr2LineCommand), Build(BuildCommand), Deploy(DeployCommand), #[structopt(name = "fmt")] @@ -42,6 +45,7 @@ enum Forc { pub(crate) async fn run_cli() -> Result<(), String> { let opt = Opt::from_args(); match opt.command { + Forc::Addr2Line(command) => addr2line::exec(command), Forc::Build(command) => build::exec(command), Forc::Deploy(command) => deploy::exec(command).await, Forc::Format(command) => format::exec(command), diff --git a/forc/src/ops/forc_build.rs b/forc/src/ops/forc_build.rs index 789e76547a0..30a1cdbb3d3 100644 --- a/forc/src/ops/forc_build.rs +++ b/forc/src/ops/forc_build.rs @@ -78,6 +78,8 @@ pub fn build(command: BuildCommand) -> Result, String> { let mut dependency_graph = HashMap::new(); let namespace = create_module(); + let mut source_map = SourceMap::new(); + if let Some(ref mut deps) = manifest.dependencies { for (dependency_name, dependency_details) in deps.iter_mut() { compile_dependency_lib( @@ -89,14 +91,22 @@ pub fn build(command: BuildCommand) -> Result, String> { silent_mode, offline_mode, )?; + + source_map.insert_dependency(match dependency_details { + Dependency::Simple(..) => { + todo!("simple deps (compile_dependency_lib should have errored on this)"); + } + Dependency::Detailed(DependencyDetails { path, .. }) => path + .as_ref() + .expect("compile_dependency_lib should have set this") + .clone(), + }); } } // now, compile this program with all of its dependencies let main_file = get_main_file(&manifest, &manifest_dir)?; - let mut source_map = SourceMap::new(); - let main = compile( main_file, &manifest.project.name, diff --git a/sway-core/Cargo.toml b/sway-core/Cargo.toml index 2b830b5a3cc..880b4b1a3fe 100644 --- a/sway-core/Cargo.toml +++ b/sway-core/Cargo.toml @@ -13,6 +13,7 @@ selector-debug = ["structopt", "hex"] [dependencies] derivative = "2.2.0" +dirs = "3.0" either = "1.6" fuel-asm = "0.1" fuel-vm = "0.2" diff --git a/sway-core/src/source_map.rs b/sway-core/src/source_map.rs index f1577c67b0e..83aeb5c74bb 100755 --- a/sway-core/src/source_map.rs +++ b/sway-core/src/source_map.rs @@ -1,5 +1,6 @@ +use dirs::home_dir; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; @@ -12,7 +13,12 @@ pub struct PathIndex(usize); #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SourceMap { + /// Paths of dependencies in the `~/.forc` directory, with the prefix stripped. + /// This makes inverse source mapping work on any machine with deps downloaded. + dependency_paths: Vec, + /// Paths to source code files, defined separately to avoid repetition. paths: Vec, + /// Mapping from opcode index to source location map: HashMap, } impl SourceMap { @@ -20,6 +26,17 @@ impl SourceMap { Self::default() } + /// Inserts dependency path. Unsupported locations are ignored for now. + pub fn insert_dependency>(&mut self, path: P) { + if let Some(home) = home_dir() { + let forc = home.join(".forc/"); + if let Ok(unprefixed) = path.as_ref().strip_prefix(forc) { + self.dependency_paths.push(unprefixed.to_owned()); + } + } + // TODO: Only dependencies in ~/.forc are supported for now + } + pub fn insert(&mut self, pc: usize, span: &Span) { if let Some(path) = span.path.as_ref() { let path_index = self @@ -42,6 +59,26 @@ impl SourceMap { ); } } + + /// Inverse source mapping + pub fn addr_to_span(&self, pc: usize) -> Option<(PathBuf, LocationRange)> { + self.map.get(&pc).map(|sms| { + let p = &self.paths[sms.path.0]; + for dep in &self.dependency_paths { + if p.starts_with(dep.file_name().unwrap()) { + let mut path = home_dir().expect("Could not get homedir").join(".forc"); + + if let Some(dp) = dep.parent() { + path = path.join(dp); + } + + return (path.join(p), sms.range); + } + } + + (p.to_owned(), sms.range) + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -50,7 +87,7 @@ pub struct SourceMapSpan { pub range: LocationRange, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct LocationRange { pub start: usize, pub end: usize,