Skip to content

Commit

Permalink
Add addr2line command for inverse source mapping (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dentosal authored Feb 11, 2022
1 parent 83ad737 commit 0b1d0e1
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

139 changes: 139 additions & 0 deletions forc/src/cli/commands/addr2line.rs
Original file line number Diff line number Diff line change
@@ -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<P: AsRef<Path>>(
path: P,
range: LocationRange,
context_lines: usize,
) -> io::Result<ReadRange> {
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,
})
}
1 change: 1 addition & 0 deletions forc/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod addr2line;
pub mod build;
pub mod deploy;
pub mod format;
Expand Down
6 changes: 5 additions & 1 deletion forc/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,8 @@ struct Opt {

#[derive(Debug, StructOpt)]
enum Forc {
#[structopt(name = "addr2line")]
Addr2Line(Addr2LineCommand),
Build(BuildCommand),
Deploy(DeployCommand),
#[structopt(name = "fmt")]
Expand All @@ -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),
Expand Down
14 changes: 12 additions & 2 deletions forc/src/ops/forc_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub fn build(command: BuildCommand) -> Result<Vec<u8>, 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(
Expand All @@ -89,14 +91,22 @@ pub fn build(command: BuildCommand) -> Result<Vec<u8>, 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,
Expand Down
1 change: 1 addition & 0 deletions sway-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.4"
Expand Down
41 changes: 39 additions & 2 deletions sway-core/src/source_map.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -12,14 +13,30 @@ 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<PathBuf>,
/// Paths to source code files, defined separately to avoid repetition.
paths: Vec<PathBuf>,
/// Mapping from opcode index to source location
map: HashMap<usize, SourceMapSpan>,
}
impl SourceMap {
pub fn new() -> Self {
Self::default()
}

/// Inserts dependency path. Unsupported locations are ignored for now.
pub fn insert_dependency<P: AsRef<Path>>(&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
Expand All @@ -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)]
Expand All @@ -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,
Expand Down

0 comments on commit 0b1d0e1

Please sign in to comment.