git.delta.rocks / jrsonnet / refs/commits / 3738ed2a5354

difftreelog

feat(ir) source url

qkmunwwmYaroslav Bolyukin2026-05-05parent: #ede8518.patch.diff
in: master

7 files changed

modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -35,14 +35,14 @@
 
 pub use ctx::*;
 pub use dynamic::*;
-pub use error::{Error, ErrorKind::*, Result, ResultExt};
+pub use error::{Error, ErrorKind::*, Result, ResultExt, StackTraceElement};
 pub use evaluate::ensure_sufficient_stack;
 use function::CallLocation;
 pub use import::*;
 use jrsonnet_gcmodule::{Cc, Trace, cc_dyn};
 pub use jrsonnet_interner::{IBytes, IStr};
 use jrsonnet_ir::Expr;
-pub use jrsonnet_ir::{NumValue, Source, SourcePath, Span};
+pub use jrsonnet_ir::{NumValue, Source, SourcePath, SourceUrl, SourceVirtual, Span};
 #[doc(hidden)]
 pub use jrsonnet_macros;
 
@@ -396,9 +396,17 @@
 			file.parsed = Some(
 				parse_jsonnet(&code, file_name.clone())
 					.map(Rc::new)
-					.map_err(|e| ImportSyntaxError {
-						path: file_name.clone(),
-						error: Box::new(e),
+					.map_err(|e| {
+						let span = e.location.clone();
+						let mut err = Error::from(ImportSyntaxError {
+							path: file_name.clone(),
+							error: Box::new(e),
+						});
+						err.trace_mut().0.push(StackTraceElement {
+							location: Some(span),
+							desc: "parse imported".to_string(),
+						});
+						err
 					})?,
 			);
 		}
modifiedcrates/jrsonnet-evaluator/src/trace/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/trace/mod.rs
1#[cfg(feature = "explaining-traces")]2use std::cell::RefCell;3use std::{4	any::Any,5	path::{Component, Path, PathBuf},6};78use jrsonnet_gcmodule::Trace;9use jrsonnet_ir::CodeLocation;10#[cfg(feature = "explaining-traces")]11use jrsonnet_ir::Span;1213use crate::{Error, error::ErrorKind};1415/// The way paths should be displayed16#[derive(Clone, Trace)]17pub enum PathResolver {18	/// Only filename19	FileName,20	/// Absolute path21	Absolute,22	/// Path relative to base directory23	Relative(PathBuf),24}2526impl PathResolver {27	/// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure28	pub fn new_cwd_fallback() -> Self {29		std::env::current_dir().map_or(Self::Absolute, Self::Relative)30	}31	pub fn resolve(&self, from: &Path) -> String {32		match self {33			Self::FileName => from34				.file_name()35				.expect("file name exists")36				.to_string_lossy()37				.into_owned(),38			Self::Absolute => from.to_string_lossy().into_owned(),39			Self::Relative(base) => {40				if from.is_relative() {41					return from.to_string_lossy().into_owned();42				}43				let diff = pathdiff::diff_paths(from, base).expect("base is absolute");44				let parents = diff45					.components()46					.take_while(|c| matches!(c, Component::ParentDir))47					.count();48				let base_depth = base49					.components()50					.filter(|c| matches!(c, Component::Normal(_)))51					.count();52				if parents > 0 && parents >= base_depth {53					return from.to_string_lossy().into_owned();54				}55				diff.to_string_lossy().into_owned()56			}57		}58	}59}6061/// Implements pretty-printing of traces62#[allow(clippy::module_name_repetitions)]63pub trait TraceFormat: Trace {64	fn write_trace(65		&self,66		out: &mut dyn std::fmt::Write,67		error: &Error,68	) -> Result<(), std::fmt::Error>;69	fn format(&self, error: &Error) -> Result<String, std::fmt::Error> {70		let mut out = String::new();71		self.write_trace(&mut out, error)?;72		Ok(out)73	}74	fn as_any(&self) -> &dyn Any;75	fn as_any_mut(&mut self) -> &mut dyn Any;76}7778fn print_code_location(79	out: &mut impl std::fmt::Write,80	start: &CodeLocation,81	end: &CodeLocation,82) -> Result<(), std::fmt::Error> {83	if start.line == end.line {84		if start.column == end.column {85			write!(out, "{}:{}", start.line, end.column.saturating_sub(1))?;86		} else {87			write!(out, "{}:{}-{}", start.line, start.column - 1, end.column)?;88		}89	} else {90		write!(91			out,92			"{}:{}-{}:{}",93			start.line,94			end.column.saturating_sub(1),95			start.line,96			end.column97		)?;98	}99	Ok(())100}101102/// vanilla-like jsonnet formatting103#[derive(Trace)]104pub struct CompactFormat {105	pub resolver: PathResolver,106	pub max_trace: usize,107	pub padding: usize,108}109impl Default for CompactFormat {110	fn default() -> Self {111		Self {112			resolver: PathResolver::Absolute,113			max_trace: 20,114			padding: 4,115		}116	}117}118119impl TraceFormat for CompactFormat {120	fn write_trace(121		&self,122		out: &mut dyn std::fmt::Write,123		error: &Error,124	) -> Result<(), std::fmt::Error> {125		write!(out, "{}", error.error())?;126		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {127			use std::fmt::Write;128129			writeln!(out)?;130			let mut n = path.source_path().path().map_or_else(131				|| path.source_path().to_string(),132				|r| self.resolver.resolve(r),133			);134			let mut offset = error.location.1 as usize;135			let is_eof = if offset >= path.code().len() {136				offset = path.code().len().saturating_sub(1);137				true138			} else {139				false140			};141			#[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]142			let mut location = path143				.map_source_locations(&[offset as u32])144				.into_iter()145				.next()146				.unwrap();147			if is_eof {148				location.column += 1;149			}150151			write!(n, ":").unwrap();152			print_code_location(&mut n, &location, &location).unwrap();153			write!(out, "{:<p$}{n}", "", p = self.padding)?;154		}155		let file_names = error156			.trace()157			.0158			.iter()159			.map(|el| &el.location)160			.map(|location| {161				use std::fmt::Write;162				#[allow(clippy::option_if_let_else)]163				if let Some(location) = location {164					let mut resolved_path = match location.0.source_path().path() {165						Some(r) => self.resolver.resolve(r),166						None => location.0.source_path().to_string(),167					};168					// TODO: Process all trace elements first169					let location = location.0.map_source_locations(&[location.1, location.2]);170					write!(resolved_path, ":").unwrap();171					print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();172					write!(resolved_path, ":").unwrap();173					Some(resolved_path)174				} else {175					None176				}177			})178			.collect::<Vec<_>>();179		let align = file_names180			.iter()181			.flatten()182			.map(String::len)183			.max()184			.unwrap_or(0);185		for (el, file) in error.trace().0.iter().zip(file_names) {186			writeln!(out)?;187			if let Some(file) = file {188				write!(189					out,190					"{:<p$}{:<w$} {}",191					"",192					file,193					el.desc,194					p = self.padding,195					w = align196				)?;197			} else {198				write!(out, "{:<p$}{}", "", el.desc, p = self.padding,)?;199			}200		}201		Ok(())202	}203204	fn as_any(&self) -> &dyn Any {205		self206	}207208	fn as_any_mut(&mut self) -> &mut dyn Any {209		self210	}211}212213#[derive(Trace)]214pub struct JsFormat {215	pub max_trace: usize,216}217impl TraceFormat for JsFormat {218	fn write_trace(219		&self,220		out: &mut dyn std::fmt::Write,221		error: &Error,222	) -> Result<(), std::fmt::Error> {223		write!(out, "{}", error.error())?;224		for item in &error.trace().0 {225			writeln!(out)?;226			let desc = &item.desc;227			if let Some(source) = &item.location {228				let start_end = source.0.map_source_locations(&[source.1, source.2]);229				let resolved_path = source.0.source_path().path().map_or_else(230					|| source.0.source_path().to_string(),231					|r| r.display().to_string(),232				);233234				write!(235					out,236					"    at {} ({}:{}:{})",237					desc, resolved_path, start_end[0].line, start_end[0].column,238				)?;239			} else {240				write!(out, "    during {desc}")?;241			}242		}243		Ok(())244	}245246	fn as_any(&self) -> &dyn Any {247		self248	}249250	fn as_any_mut(&mut self) -> &mut dyn Any {251		self252	}253}254255#[cfg(feature = "explaining-traces")]256#[derive(Trace)]257pub struct HiDocFormat {258	pub resolver: PathResolver,259	pub max_trace: usize,260}261#[cfg(feature = "explaining-traces")]262impl TraceFormat for HiDocFormat {263	fn write_trace(264		&self,265		out: &mut dyn std::fmt::Write,266		error: &Error,267	) -> Result<(), std::fmt::Error> {268		struct ResetData {269			loc: Span,270		}271		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};272273		write!(out, "{}", error.error())?;274		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {275			writeln!(out)?;276			let mut builder = SnippetBuilder::new(path.code());277			builder278				.error(Text::fragment("syntax error", Formatting::default()))279				.range(error.location.range())280				.build();281			let source = builder.build();282			let ansi = source_to_ansi(&source);283			write!(out, "{ansi}")?;284		}285		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {286			use crate::analyze::DiagLevel;287			let mut builder: Option<SnippetBuilder> = None;288			let mut current_src: Option<&str> = None;289			let flush = |builder: Option<SnippetBuilder>,290			             out: &mut dyn std::fmt::Write|291			 -> Result<(), std::fmt::Error> {292				if let Some(b) = builder {293					let ansi = source_to_ansi(&b.build());294					write!(out, "\n{}", ansi.trim_end())?;295				}296				Ok(())297			};298			for diag in diagnostics {299				if let Some(span) = &diag.span {300					let src = span.0.code();301					if current_src != Some(src) {302						flush(builder.take(), out)?;303						builder = Some(SnippetBuilder::new(src));304						current_src = Some(src);305					}306					let b = builder.as_mut().unwrap();307					let ab = match diag.level {308						DiagLevel::Error => {309							b.error(Text::fragment(diag.message.clone(), Formatting::default()))310						}311						DiagLevel::Warning => {312							b.warning(Text::fragment(diag.message.clone(), Formatting::default()))313						}314					};315					ab.range(span.range()).build();316				} else {317					flush(builder.take(), out)?;318					current_src = None;319					let prefix = match diag.level {320						DiagLevel::Error => "error",321						DiagLevel::Warning => "warning",322					};323					write!(out, "\n{prefix}: {}", diag.message)?;324				}325			}326			flush(builder, out)?;327		}328		let trace = &error.trace();329		let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);330		let mut last_location: Option<Span> = None;331		let mut flush_builder = |data: Option<ResetData>| {332			use std::fmt::Write;333			let mut out = String::new();334			let location_changed = if let Some(ResetData { loc }) = &data {335				if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {336					true337				} else if let (Some(last), new) = (&last_location, loc) {338					// Reverse condition if traceback339					last.1 > new.1 || last.2 > new.2340				} else {341					false342				}343			} else {344				true345			};346			if location_changed {347				if let Some(builder) = snippet_builder.borrow_mut().take() {348					let rendered = builder.build();349					let ansi = source_to_ansi(&rendered);350					if let Some(loc) = &last_location {351						let _ = writeln!(out, "...at {}", loc.0.source_path());352					}353					let _ = write!(out, "{}", ansi.trim_end());354				}355				last_location = None;356357				if let Some(ResetData { loc }) = data {358					*snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));359					last_location = Some(loc);360				}361			}362			if out.is_empty() {363				return None;364			}365			Some(out)366		};367		for item in &trace.0 {368			let desc = &item.desc;369			if let Some(source) = &item.location {370				if let Some(flushed) = flush_builder(Some(ResetData {371					loc: source.clone(),372				})) {373					writeln!(out)?;374					write!(out, "{flushed}")?;375				}376				let mut builder = snippet_builder.borrow_mut();377				let builder = builder.as_mut().unwrap();378				builder379					.note(Text::fragment(desc, Formatting::default()))380					.range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))381					.build();382			} else {383				if let Some(flushed) = flush_builder(None) {384					writeln!(out)?;385					write!(out, "{flushed}")?;386				}387				writeln!(out)?;388				write!(out, "   {desc}")?;389			}390		}391392		if let Some(flushed) = flush_builder(None) {393			writeln!(out)?;394			write!(out, "{flushed}")?;395		}396		Ok(())397	}398399	fn as_any(&self) -> &dyn Any {400		self401	}402403	fn as_any_mut(&mut self) -> &mut dyn Any {404		self405	}406}
after · crates/jrsonnet-evaluator/src/trace/mod.rs
1#[cfg(feature = "explaining-traces")]2use std::cell::RefCell;3use std::{4	any::Any,5	path::{Component, Path, PathBuf},6};78use jrsonnet_gcmodule::Trace;9use jrsonnet_ir::CodeLocation;10#[cfg(feature = "explaining-traces")]11use jrsonnet_ir::Span;1213use crate::{Error, error::ErrorKind};1415/// The way paths should be displayed16#[derive(Clone, Trace)]17pub enum PathResolver {18	/// Only filename19	FileName,20	/// Absolute path21	Absolute,22	/// Path relative to base directory23	Relative(PathBuf),24}2526impl PathResolver {27	/// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure28	pub fn new_cwd_fallback() -> Self {29		std::env::current_dir().map_or(Self::Absolute, Self::Relative)30	}31	pub fn resolve(&self, from: &Path) -> String {32		match self {33			Self::FileName => from34				.file_name()35				.expect("file name exists")36				.to_string_lossy()37				.into_owned(),38			Self::Absolute => from.to_string_lossy().into_owned(),39			Self::Relative(base) => {40				if from.is_relative() {41					return from.to_string_lossy().into_owned();42				}43				let diff = pathdiff::diff_paths(from, base).expect("base is absolute");44				let parents = diff45					.components()46					.take_while(|c| matches!(c, Component::ParentDir))47					.count();48				let base_depth = base49					.components()50					.filter(|c| matches!(c, Component::Normal(_)))51					.count();52				if parents > 0 && parents >= base_depth {53					return from.to_string_lossy().into_owned();54				}55				diff.to_string_lossy().into_owned()56			}57		}58	}59}6061/// Implements pretty-printing of traces62#[allow(clippy::module_name_repetitions)]63pub trait TraceFormat: Trace {64	fn write_trace(65		&self,66		out: &mut dyn std::fmt::Write,67		error: &Error,68	) -> Result<(), std::fmt::Error>;69	fn format(&self, error: &Error) -> Result<String, std::fmt::Error> {70		let mut out = String::new();71		self.write_trace(&mut out, error)?;72		Ok(out)73	}74	fn as_any(&self) -> &dyn Any;75	fn as_any_mut(&mut self) -> &mut dyn Any;76}7778fn print_code_location(79	out: &mut impl std::fmt::Write,80	start: &CodeLocation,81	end: &CodeLocation,82) -> Result<(), std::fmt::Error> {83	if start.line == end.line {84		if start.column == end.column {85			write!(out, "{}:{}", start.line, start.column)?;86		} else {87			write!(88				out,89				"{}:{}-{}",90				start.line,91				start.column,92				end.column.saturating_sub(1)93			)?;94		}95	} else {96		write!(97			out,98			"{}:{}-{}:{}",99			start.line,100			start.column,101			end.line,102			end.column.saturating_sub(1)103		)?;104	}105	Ok(())106}107108/// vanilla-like jsonnet formatting109#[derive(Trace)]110pub struct CompactFormat {111	pub resolver: PathResolver,112	pub max_trace: usize,113	pub padding: usize,114}115impl Default for CompactFormat {116	fn default() -> Self {117		Self {118			resolver: PathResolver::Absolute,119			max_trace: 20,120			padding: 4,121		}122	}123}124125impl TraceFormat for CompactFormat {126	fn write_trace(127		&self,128		out: &mut dyn std::fmt::Write,129		error: &Error,130	) -> Result<(), std::fmt::Error> {131		write!(out, "{}", error.error())?;132		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {133			use std::fmt::Write;134135			writeln!(out)?;136			let mut n = path.source_path().path().map_or_else(137				|| path.source_path().to_string(),138				|r| self.resolver.resolve(r),139			);140			let offset = (error.location.1 as usize).min(path.code().len());141			#[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]142			let location = path143				.map_source_locations(&[offset as u32])144				.into_iter()145				.next()146				.unwrap();147148			write!(n, ":").unwrap();149			print_code_location(&mut n, &location, &location).unwrap();150			write!(out, "{:<p$}{n}", "", p = self.padding)?;151		}152		let file_names = error153			.trace()154			.0155			.iter()156			.map(|el| &el.location)157			.map(|location| {158				use std::fmt::Write;159				#[allow(clippy::option_if_let_else)]160				if let Some(location) = location {161					let mut resolved_path = match location.0.source_path().path() {162						Some(r) => self.resolver.resolve(r),163						None => location.0.source_path().to_string(),164					};165					// TODO: Process all trace elements first166					let location = location.0.map_source_locations(&[location.1, location.2]);167					write!(resolved_path, ":").unwrap();168					print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();169					write!(resolved_path, ":").unwrap();170					Some(resolved_path)171				} else {172					None173				}174			})175			.collect::<Vec<_>>();176		let align = file_names177			.iter()178			.flatten()179			.map(String::len)180			.max()181			.unwrap_or(0);182		for (el, file) in error.trace().0.iter().zip(file_names) {183			writeln!(out)?;184			if let Some(file) = file {185				write!(186					out,187					"{:<p$}{:<w$} {}",188					"",189					file,190					el.desc,191					p = self.padding,192					w = align193				)?;194			} else {195				write!(out, "{:<p$}{}", "", el.desc, p = self.padding,)?;196			}197		}198		Ok(())199	}200201	fn as_any(&self) -> &dyn Any {202		self203	}204205	fn as_any_mut(&mut self) -> &mut dyn Any {206		self207	}208}209210#[derive(Trace)]211pub struct JsFormat {212	pub max_trace: usize,213}214impl TraceFormat for JsFormat {215	fn write_trace(216		&self,217		out: &mut dyn std::fmt::Write,218		error: &Error,219	) -> Result<(), std::fmt::Error> {220		write!(out, "{}", error.error())?;221		for item in &error.trace().0 {222			writeln!(out)?;223			let desc = &item.desc;224			if let Some(source) = &item.location {225				let start_end = source.0.map_source_locations(&[source.1, source.2]);226				let resolved_path = source.0.source_path().path().map_or_else(227					|| source.0.source_path().to_string(),228					|r| r.display().to_string(),229				);230231				write!(232					out,233					"    at {} ({}:{}:{})",234					desc, resolved_path, start_end[0].line, start_end[0].column,235				)?;236			} else {237				write!(out, "    during {desc}")?;238			}239		}240		Ok(())241	}242243	fn as_any(&self) -> &dyn Any {244		self245	}246247	fn as_any_mut(&mut self) -> &mut dyn Any {248		self249	}250}251252#[cfg(feature = "explaining-traces")]253#[derive(Trace)]254pub struct HiDocFormat {255	pub resolver: PathResolver,256	pub max_trace: usize,257}258#[cfg(feature = "explaining-traces")]259impl TraceFormat for HiDocFormat {260	fn write_trace(261		&self,262		out: &mut dyn std::fmt::Write,263		error: &Error,264	) -> Result<(), std::fmt::Error> {265		struct ResetData {266			loc: Span,267		}268		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};269270		write!(out, "{}", error.error())?;271		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {272			writeln!(out)?;273			let mut builder = SnippetBuilder::new(path.code());274			builder275				.error(Text::fragment("syntax error", Formatting::default()))276				.range(error.location.range())277				.build();278			let source = builder.build();279			let ansi = source_to_ansi(&source);280			write!(out, "{ansi}")?;281		}282		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {283			use crate::analyze::DiagLevel;284			let mut builder: Option<SnippetBuilder> = None;285			let mut current_src: Option<&str> = None;286			let flush = |builder: Option<SnippetBuilder>,287			             out: &mut dyn std::fmt::Write|288			 -> Result<(), std::fmt::Error> {289				if let Some(b) = builder {290					let ansi = source_to_ansi(&b.build());291					write!(out, "\n{}", ansi.trim_end())?;292				}293				Ok(())294			};295			for diag in diagnostics {296				if let Some(span) = &diag.span {297					let src = span.0.code();298					if current_src != Some(src) {299						flush(builder.take(), out)?;300						builder = Some(SnippetBuilder::new(src));301						current_src = Some(src);302					}303					let b = builder.as_mut().unwrap();304					let ab = match diag.level {305						DiagLevel::Error => {306							b.error(Text::fragment(diag.message.clone(), Formatting::default()))307						}308						DiagLevel::Warning => {309							b.warning(Text::fragment(diag.message.clone(), Formatting::default()))310						}311					};312					ab.range(span.range()).build();313				} else {314					flush(builder.take(), out)?;315					current_src = None;316					let prefix = match diag.level {317						DiagLevel::Error => "error",318						DiagLevel::Warning => "warning",319					};320					write!(out, "\n{prefix}: {}", diag.message)?;321				}322			}323			flush(builder, out)?;324		}325		let trace = &error.trace();326		let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);327		let mut last_location: Option<Span> = None;328		let mut flush_builder = |data: Option<ResetData>| {329			use std::fmt::Write;330			let mut out = String::new();331			let location_changed = if let Some(ResetData { loc }) = &data {332				if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {333					true334				} else if let (Some(last), new) = (&last_location, loc) {335					// Reverse condition if traceback336					last.1 > new.1 || last.2 > new.2337				} else {338					false339				}340			} else {341				true342			};343			if location_changed {344				if let Some(builder) = snippet_builder.borrow_mut().take() {345					let rendered = builder.build();346					let ansi = source_to_ansi(&rendered);347					if let Some(loc) = &last_location {348						let _ = writeln!(out, "...at {}", loc.0.source_path());349					}350					let _ = write!(out, "{}", ansi.trim_end());351				}352				last_location = None;353354				if let Some(ResetData { loc }) = data {355					*snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));356					last_location = Some(loc);357				}358			}359			if out.is_empty() {360				return None;361			}362			Some(out)363		};364		for item in &trace.0 {365			let desc = &item.desc;366			if let Some(source) = &item.location {367				if let Some(flushed) = flush_builder(Some(ResetData {368					loc: source.clone(),369				})) {370					writeln!(out)?;371					write!(out, "{flushed}")?;372				}373				let mut builder = snippet_builder.borrow_mut();374				let builder = builder.as_mut().unwrap();375				builder376					.note(Text::fragment(desc, Formatting::default()))377					.range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))378					.build();379			} else {380				if let Some(flushed) = flush_builder(None) {381					writeln!(out)?;382					write!(out, "{flushed}")?;383				}384				writeln!(out)?;385				write!(out, "   {desc}")?;386			}387		}388389		if let Some(flushed) = flush_builder(None) {390			writeln!(out)?;391			write!(out, "{flushed}")?;392		}393		Ok(())394	}395396	fn as_any(&self) -> &dyn Any {397		self398	}399400	fn as_any_mut(&mut self) -> &mut dyn Any {401		self402	}403}
modifiedcrates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -895,10 +895,16 @@
 	}
 }
 
+#[derive(Default)]
 pub struct FormatOptions {
 	// 0 for hard tabs, otherwise number of spaces
 	pub indent: u8,
 }
+impl FormatOptions {
+	pub fn new() -> Self {
+		Self::default()
+	}
+}
 
 #[allow(
 	clippy::result_large_err,
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -220,10 +220,7 @@
 
 fn ident(p: &mut Parser<'_>) -> Result<IStr> {
 	if !p.at(SyntaxKind::IDENT) {
-		return Err(p.error(format!(
-			"expected identifier, got {}",
-			p.current_desc()
-		)));
+		return Err(p.error(format!("expected identifier, got {}", p.current_desc())));
 	}
 	let text = p.text();
 	p.eat_any();
modifiedcrates/jrsonnet-ir/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/lib.rs
+++ b/crates/jrsonnet-ir/src/lib.rs
@@ -15,7 +15,7 @@
 pub use location::CodeLocation;
 pub use source::{
 	Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,
-	SourcePathT, SourceVirtual,
+	SourcePathT, SourceUrl, SourceVirtual,
 };
 
 // It seels to be a wrong place for this kind of stuff, but as it would also be used for static analysis and
modifiedcrates/jrsonnet-ir/src/location.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/location.rs
+++ b/crates/jrsonnet-ir/src/location.rs
@@ -29,7 +29,7 @@
 		return [CodeLocation::default(); S];
 	}
 	let mut line = 1;
-	let mut column = 1;
+	let mut column = 0;
 	let max_offset = *offsets.iter().max().expect("offsets is not empty");
 
 	let mut offset_map = offsets
@@ -63,7 +63,7 @@
 		}
 		if ch == '\n' {
 			line += 1;
-			column = 1;
+			column = 0;
 
 			for idx in with_no_known_line_ending.drain(..) {
 				out[idx].line_end_offset = pos;
@@ -98,14 +98,14 @@
 				CodeLocation {
 					offset: 0,
 					line: 1,
-					column: 2,
+					column: 1,
 					line_start_offset: 0,
 					line_end_offset: 11,
 				},
 				CodeLocation {
 					offset: 14,
 					line: 2,
-					column: 4,
+					column: 3,
 					line_start_offset: 12,
 					line_end_offset: 67
 				}
modifiedcrates/jrsonnet-ir/src/source.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/source.rs
+++ b/crates/jrsonnet-ir/src/source.rs
@@ -8,6 +8,7 @@
 
 use jrsonnet_gcmodule::Acyclic;
 use jrsonnet_interner::{IBytes, IStr};
+use url::Url;
 
 use crate::location::{CodeLocation, location_to_offset, offset_to_location};
 
@@ -186,6 +187,28 @@
 	any_ext_impl!(SourcePathT);
 }
 
+#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
+pub struct SourceUrl(Url);
+impl SourceUrl {
+	pub fn new(url: Url) -> Self {
+		Self(url)
+	}
+}
+impl Display for SourceUrl {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.0)
+	}
+}
+impl SourcePathT for SourceUrl {
+	fn is_default(&self) -> bool {
+		false
+	}
+	fn path(&self) -> Option<&Path> {
+		None
+	}
+	any_ext_impl!(SourcePathT);
+}
+
 /// Represents path to the directory on the disk
 ///
 /// See also [`SourceFile`]