Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

addr2line command for inverse source mapping #741

Merged
merged 1 commit into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.2"
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