--- a/crates/jrsonnet-evaluator/Cargo.toml +++ b/crates/jrsonnet-evaluator/Cargo.toml @@ -9,7 +9,7 @@ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["serialized-stdlib", "faster"] +default = ["serialized-stdlib", "faster", "explaining-traces"] # Serializes standard library AST instead of parsing them every run serialized-stdlib = ["serde", "bincode", "jrsonnet-parser/deserialize"] # Same as above, but with generated code instead of serde. Reduces memory usage, but increases binary size and compilation time @@ -17,17 +17,32 @@ # Replace some standard library functions with faster implementations (I.e manifestJsonEx) # Library works fine without this feature, but requires more memory and time for std function calls faster = [] +# Rustc-like trace visualization +explaining-traces = ["annotate-snippets"] [dependencies] jrsonnet-parser = { path = "../jrsonnet-parser", version = "1.0.0" } +jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "1.0.0" } +pathdiff = "0.2.0" + closure = "0.3.0" -jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "1.0.0" } indexmap = "1.4.0" + md5 = "0.7.0" base64 = "0.12.3" -serde = { version = "1.0.114", optional = true } -bincode = { version = "1.3.1", optional = true } +# Serialized stdlib +[dependencies.serde] +version = "1.0.114" +optional = true +[dependencies.bincode] +version = "1.3.1" +optional = true + +# Explaining traces +[dependencies.annotate-snippets] +version = "0.9.0" +optional = true [build-dependencies] jrsonnet-parser = { path = "../jrsonnet-parser", features = ["dump", "serialize", "deserialize"], version = "1.0.0" } --- a/crates/jrsonnet-evaluator/src/trace.rs +++ /dev/null @@ -1,99 +0,0 @@ -#[derive(Clone, PartialEq, Debug)] -pub struct CodeLocation { - pub line: usize, - pub column: usize, - - pub line_start_offset: usize, - pub line_end_offset: usize, -} - -pub fn offset_to_location(file: &str, offsets: &[usize]) -> Vec { - if offsets.is_empty() { - return vec![]; - } - let mut line = 1; - let mut column = 1; - let max_offset = *offsets.iter().max().unwrap(); - - let mut offset_map = offsets - .iter() - .enumerate() - .map(|(pos, offset)| (*offset, pos)) - .collect::>(); - offset_map.sort_by_key(|v| v.0); - offset_map.reverse(); - - let mut out = vec![ - CodeLocation { - column: 0, - line: 0, - line_start_offset: 0, - line_end_offset: 0 - }; - offsets.len() - ]; - let mut with_no_known_line_ending = vec![]; - let mut this_line_offset = 0; - for (pos, ch) in file.chars().enumerate() { - column += 1; - match offset_map.last() { - Some(x) if x.0 == pos => { - let out_idx = x.1; - with_no_known_line_ending.push(out_idx); - out[out_idx].line = line; - out[out_idx].column = column; - out[out_idx].line_start_offset = this_line_offset; - offset_map.pop(); - } - _ => {} - } - if ch == '\n' { - line += 1; - column = 1; - - for idx in with_no_known_line_ending.drain(..) { - out[idx].line_end_offset = pos; - } - this_line_offset = pos + 1; - - if pos == max_offset + 1 { - break; - } - } - } - let file_end = file.chars().count(); - for idx in with_no_known_line_ending { - out[idx].line_end_offset = file_end; - } - - out -} - -#[cfg(test)] -pub mod tests { - use super::{offset_to_location, CodeLocation}; - - #[test] - fn test() { - assert_eq!( - offset_to_location( - "hello world\n_______________________________________________________", - &[0, 14] - ), - vec![ - CodeLocation { - line: 1, - column: 1, - line_start_offset: 0, - line_end_offset: 11 - }, - CodeLocation { - line: 2, - column: 3, - line_start_offset: 11, - line_end_offset: 67 - } - ] - ) - } -} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/trace/location.rs @@ -0,0 +1,99 @@ +#[derive(Clone, PartialEq, Debug)] +pub struct CodeLocation { + pub line: usize, + pub column: usize, + + pub line_start_offset: usize, + pub line_end_offset: usize, +} + +pub fn offset_to_location(file: &str, offsets: &[usize]) -> Vec { + if offsets.is_empty() { + return vec![]; + } + let mut line = 1; + let mut column = 1; + let max_offset = *offsets.iter().max().unwrap(); + + let mut offset_map = offsets + .iter() + .enumerate() + .map(|(pos, offset)| (*offset, pos)) + .collect::>(); + offset_map.sort_by_key(|v| v.0); + offset_map.reverse(); + + let mut out = vec![ + CodeLocation { + column: 0, + line: 0, + line_start_offset: 0, + line_end_offset: 0 + }; + offsets.len() + ]; + let mut with_no_known_line_ending = vec![]; + let mut this_line_offset = 0; + for (pos, ch) in file.chars().enumerate() { + column += 1; + match offset_map.last() { + Some(x) if x.0 == pos => { + let out_idx = x.1; + with_no_known_line_ending.push(out_idx); + out[out_idx].line = line; + out[out_idx].column = column; + out[out_idx].line_start_offset = this_line_offset; + offset_map.pop(); + } + _ => {} + } + if ch == '\n' { + line += 1; + column = 1; + + for idx in with_no_known_line_ending.drain(..) { + out[idx].line_end_offset = pos; + } + this_line_offset = pos + 1; + + if pos == max_offset + 1 { + break; + } + } + } + let file_end = file.chars().count(); + for idx in with_no_known_line_ending { + out[idx].line_end_offset = file_end; + } + + out +} + +#[cfg(test)] +pub mod tests { + use super::{offset_to_location, CodeLocation}; + + #[test] + fn test() { + assert_eq!( + offset_to_location( + "hello world\n_______________________________________________________", + &[0, 14] + ), + vec![ + CodeLocation { + line: 1, + column: 1, + line_start_offset: 0, + line_end_offset: 11 + }, + CodeLocation { + line: 2, + column: 3, + line_start_offset: 11, + line_end_offset: 67 + } + ] + ) + } +} --- /dev/null +++ b/crates/jrsonnet-evaluator/src/trace/mod.rs @@ -0,0 +1,217 @@ +mod location; + +use crate::{EvaluationState, LocError}; +pub use location::*; +use std::path::PathBuf; + +/// How paths should be displayed +pub enum PathResolver { + /// Only filename will be shown + FileName, + /// Absolute path of file + Absolute, + /// Relative path from base directory + Relative(PathBuf), +} + +impl PathResolver { + pub fn resolve(&self, from: &PathBuf) -> String { + match self { + PathResolver::FileName => from.file_name().unwrap().to_string_lossy().into_owned(), + PathResolver::Absolute => from.to_string_lossy().into_owned(), + PathResolver::Relative(base) => { + if from.is_relative() { + return from.to_string_lossy().into_owned(); + } + pathdiff::diff_paths(from, base) + .unwrap() + .to_string_lossy() + .into_owned() + } + } + } +} + +/// Implements trace to string pretty-printing +pub trait TraceFormat { + fn write_trace( + &self, + out: &mut dyn std::fmt::Write, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::fmt::Error>; + // fn print_trace( + // &self, + // evaluation_state: &EvaluationState, + // error: &LocError, + // ) -> Result<(), std::fmt::Error> { + // self.write_trace(&mut std::fmt::stdout(), evaluation_state, error) + // } +} + +fn print_code_location( + out: &mut impl std::fmt::Write, + start: &CodeLocation, + end: &CodeLocation, +) -> Result<(), std::fmt::Error> { + if start.line == end.line { + if start.column == end.column { + write!(out, "{}:{}", start.line, end.column - 1)?; + } else { + write!(out, "{}:{}-{}", start.line, start.column - 1, end.column)?; + } + } else { + write!( + out, + "{}:{}-{}:{}", + start.line, + end.column - 1, + start.line, + end.column + )?; + } + Ok(()) +} + +/// vanilla jsonnet like formatting +pub struct CompactFormat { + pub resolver: PathResolver, + pub padding: usize, +} + +impl TraceFormat for CompactFormat { + fn write_trace( + &self, + out: &mut dyn std::fmt::Write, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::fmt::Error> { + writeln!(out, "{:?}", error.0)?; + let file_names = (error.1) + .0 + .iter() + .map(|el| { + let resolved_path = self.resolver.resolve(&el.location.0); + // TODO: Process all trace elements first + let location = evaluation_state + .map_source_locations(&el.location.0, &[el.location.1, el.location.2]); + (resolved_path, location) + }) + .map(|(mut n, location)| { + use std::fmt::Write; + write!(n, ":").unwrap(); + print_code_location(&mut n, &location[0], &location[1]).unwrap(); + n + }) + .collect::>(); + let align = file_names.iter().map(|e| e.len()).max().unwrap_or(0); + for (i, (el, file)) in (error.1).0.iter().zip(file_names).enumerate() { + if i != 0 { + writeln!(out)?; + } + write!( + out, + "{: Result<(), std::fmt::Error> { + writeln!(out, "{:?}", error.0)?; + for (i, item) in (error.1).0.iter().enumerate() { + if i != 0 { + writeln!(out)?; + } + let desc = &item.desc; + let source = item.location.clone(); + let start_end = evaluation_state.map_source_locations(&source.0, &[source.1, source.2]); + + write!( + out, + " at {} ({}:{}:{})", + desc, + source.0.to_str().unwrap(), + start_end[0].line, + start_end[0].column, + )?; + } + Ok(()) + } +} + +/// rustc-like trace displaying +#[cfg(feature = "explaining-traces")] +pub struct ExplainingFormat { + pub resolver: PathResolver, +} +#[cfg(feature = "explaining-traces")] +impl TraceFormat for ExplainingFormat { + fn write_trace( + &self, + out: &mut dyn std::fmt::Write, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::fmt::Error> { + use annotate_snippets::{ + display_list::{DisplayList, FormatOptions}, + snippet::{AnnotationType, Slice, Snippet, SourceAnnotation}, + }; + writeln!(out, "{:?}", error.0)?; + let trace = &error.1; + for item in trace.0.iter() { + let desc = &item.desc; + let source = item.location.clone(); + let start_end = evaluation_state.map_source_locations(&source.0, &[source.1, source.2]); + + let source_fragment: String = evaluation_state + .get_source(&source.0) + .unwrap() + .chars() + .skip(start_end[0].line_start_offset) + .take(start_end[1].line_end_offset - start_end[0].line_start_offset) + .collect(); + + let origin = self.resolver.resolve(&source.0); + let snippet = Snippet { + opt: FormatOptions { + color: true, + ..Default::default() + }, + title: None, + footer: vec![], + slices: vec![Slice { + source: &source_fragment, + line_start: start_end[0].line, + origin: Some(&origin), + fold: false, + annotations: vec![SourceAnnotation { + label: desc, + annotation_type: AnnotationType::Error, + range: ( + source.1 - start_end[0].line_start_offset, + source.2 - start_end[0].line_start_offset, + ), + }], + }], + }; + + let dl = DisplayList::from(snippet); + writeln!(out, "{}", dl)?; + } + Ok(()) + } +} --- a/crates/jrsonnet-trace/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "jrsonnet-trace" -version = "1.0.0" -authors = ["Лач "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -jrsonnet-evaluator = { path = "../jrsonnet-evaluator", version = "1.0.0" } -jrsonnet-parser = { path = "../jrsonnet-parser", version = "1.0.0" } -pathdiff = "0.2.0" -annotate-snippets = "0.9.0" --- a/crates/jrsonnet-trace/src/lib.rs +++ /dev/null @@ -1,173 +0,0 @@ -use jrsonnet_evaluator::{trace::CodeLocation, EvaluationState, LocError}; -use std::path::PathBuf; - -/// How paths should be displayed -pub enum PathResolver { - /// Only filename will be shown - FileName, - /// Absolute path of file - Absolute, - /// Relative path from base directory - Relative(PathBuf), -} - -impl PathResolver { - pub fn resolve(&self, from: &PathBuf) -> String { - match self { - PathResolver::FileName => from.file_name().unwrap().to_string_lossy().into_owned(), - PathResolver::Absolute => from.to_string_lossy().into_owned(), - PathResolver::Relative(base) => { - if from.is_relative() { - return from.to_string_lossy().into_owned(); - } - pathdiff::diff_paths(from, base) - .unwrap() - .to_string_lossy() - .into_owned() - } - } - } -} - -/// Implements trace to string pretty-printing -pub trait TraceFormat { - fn write_trace( - &self, - out: &mut dyn std::io::Write, - evaluation_state: &EvaluationState, - error: &LocError, - ) -> Result<(), std::io::Error>; - fn print_trace( - &self, - evaluation_state: &EvaluationState, - error: &LocError, - ) -> Result<(), std::io::Error> { - self.write_trace(&mut std::io::stdout(), evaluation_state, error) - } -} - -fn print_code_location( - out: &mut impl std::fmt::Write, - start: &CodeLocation, - end: &CodeLocation, -) -> Result<(), std::fmt::Error> { - if start.line == end.line { - if start.column == end.column { - write!(out, "{}:{}", start.line, end.column - 1)?; - } else { - write!(out, "{}:{}-{}", start.line, start.column - 1, end.column)?; - } - } else { - write!( - out, - "{}:{}-{}:{}", - start.line, - end.column - 1, - start.line, - end.column - )?; - } - Ok(()) -} - -/// vanilla jsonnet like formatting -pub struct CompactFormat { - pub resolver: PathResolver, -} - -impl TraceFormat for CompactFormat { - fn write_trace( - &self, - out: &mut dyn std::io::Write, - evaluation_state: &EvaluationState, - error: &LocError, - ) -> Result<(), std::io::Error> { - writeln!(out, "{:?}", error.0)?; - let file_names = (error.1) - .0 - .iter() - .map(|el| { - let resolved_path = self.resolver.resolve(&(el.0).0); - // TODO: Process all trace elements first - let location = - evaluation_state.map_source_locations(&(el.0).0, &[(el.0).1, (el.0).2]); - (resolved_path, location) - }) - .map(|(mut n, location)| { - use std::fmt::Write; - write!(n, ":").unwrap(); - print_code_location(&mut n, &location[0], &location[1]).unwrap(); - n - }) - .collect::>(); - let align = file_names.iter().map(|e| e.len()).max().unwrap_or(0); - for (i, (el, file)) in (error.1).0.iter().zip(file_names).enumerate() { - if i != 0 { - writeln!(out)?; - } - write!(out, "{: Result<(), std::io::Error> { - use annotate_snippets::{ - display_list::{DisplayList, FormatOptions}, - snippet::{AnnotationType, Slice, Snippet, SourceAnnotation}, - }; - writeln!(out, "{:?}", error.0)?; - let trace = &error.1; - for item in trace.0.iter() { - let desc = &item.1; - let source = item.0.clone(); - let start_end = evaluation_state.map_source_locations(&source.0, &[source.1, source.2]); - - let source_fragment: String = evaluation_state - .get_source(&source.0) - .unwrap() - .chars() - .skip(start_end[0].line_start_offset) - .take(start_end[1].line_end_offset - start_end[0].line_start_offset) - .collect(); - - let origin = self.resolver.resolve(&source.0); - let snippet = Snippet { - opt: FormatOptions { - color: true, - ..Default::default() - }, - title: None, - footer: vec![], - slices: vec![Slice { - source: &source_fragment, - line_start: start_end[0].line, - origin: Some(&origin), - fold: false, - annotations: vec![SourceAnnotation { - label: desc, - annotation_type: AnnotationType::Error, - range: ( - source.1 - start_end[0].line_start_offset, - source.2 - start_end[0].line_start_offset, - ), - }], - }], - }; - - let dl = DisplayList::from(snippet); - writeln!(out, "{}", dl)?; - } - Ok(()) - } -}