More type safety for spans

This commit is contained in:
Laurenz 2023-08-29 17:35:35 +02:00
parent 7bdf1f57b0
commit a71a2057f2
24 changed files with 244 additions and 279 deletions

1
Cargo.lock generated
View File

@ -2990,6 +2990,7 @@ version = "0.7.0"
dependencies = [ dependencies = [
"clap", "clap",
"comemo", "comemo",
"ecow",
"iai", "iai",
"once_cell", "once_cell",
"oxipng", "oxipng",

View File

@ -8,8 +8,8 @@ use typst::diag::{bail, Severity, SourceDiagnostic, StrResult};
use typst::doc::Document; use typst::doc::Document;
use typst::eval::{eco_format, Tracer}; use typst::eval::{eco_format, Tracer};
use typst::geom::Color; use typst::geom::Color;
use typst::syntax::{FileId, Source}; use typst::syntax::{FileId, Source, Span};
use typst::World; use typst::{World, WorldExt};
use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat}; use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat};
use crate::watch::Status; use crate::watch::Status;
@ -231,19 +231,16 @@ pub fn print_diagnostics(
.map(|e| (eco_format!("hint: {e}")).into()) .map(|e| (eco_format!("hint: {e}")).into())
.collect(), .collect(),
) )
.with_labels(vec![Label::primary( .with_labels(label(world, diagnostic.span).into_iter().collect());
diagnostic.span.id(),
world.range(diagnostic.span),
)]);
term::emit(&mut w, &config, world, &diag)?; term::emit(&mut w, &config, world, &diag)?;
// Stacktrace-like helper diagnostics. // Stacktrace-like helper diagnostics.
for point in &diagnostic.trace { for point in &diagnostic.trace {
let message = point.v.to_string(); let message = point.v.to_string();
let help = Diagnostic::help().with_message(message).with_labels(vec![ let help = Diagnostic::help()
Label::primary(point.span.id(), world.range(point.span)), .with_message(message)
]); .with_labels(label(world, point.span).into_iter().collect());
term::emit(&mut w, &config, world, &help)?; term::emit(&mut w, &config, world, &help)?;
} }
@ -252,6 +249,11 @@ pub fn print_diagnostics(
Ok(()) Ok(())
} }
/// Create a label for a span.
fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
Some(Label::primary(span.id()?, world.range(span)?))
}
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
type FileId = FileId; type FileId = FileId;
type Name = String; type Name = String;

View File

@ -935,7 +935,7 @@ pub fn plugin(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Plugin> { ) -> SourceResult<Plugin> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
Plugin::new(data).at(span) Plugin::new(data).at(span)
} }

View File

@ -37,7 +37,7 @@ pub fn read(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Readable> { ) -> SourceResult<Readable> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
Ok(match encoding { Ok(match encoding {
None => Readable::Bytes(data), None => Readable::Bytes(data),
@ -130,7 +130,7 @@ pub fn csv(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
csv_decode(Spanned::new(Readable::Bytes(data), span), delimiter) csv_decode(Spanned::new(Readable::Bytes(data), span), delimiter)
} }
@ -262,7 +262,7 @@ pub fn json(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
json_decode(Spanned::new(Readable::Bytes(data), span)) json_decode(Spanned::new(Readable::Bytes(data), span))
} }
@ -350,7 +350,7 @@ pub fn toml(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
toml_decode(Spanned::new(Readable::Bytes(data), span)) toml_decode(Spanned::new(Readable::Bytes(data), span))
@ -462,7 +462,7 @@ pub fn yaml(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
yaml_decode(Spanned::new(Readable::Bytes(data), span)) yaml_decode(Spanned::new(Readable::Bytes(data), span))
} }
@ -568,7 +568,7 @@ pub fn xml(
vm: &mut Vm, vm: &mut Vm,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
xml_decode(Spanned::new(Readable::Bytes(data), span)) xml_decode(Spanned::new(Readable::Bytes(data), span))
} }

View File

@ -58,7 +58,7 @@ pub struct BibliographyElem {
let data = paths.0 let data = paths.0
.iter() .iter()
.map(|path| { .map(|path| {
let id = vm.location().join(path).at(span)?; let id = vm.resolve_path(path).at(span)?;
vm.world().file(id).at(span) vm.world().file(id).at(span)
}) })
.collect::<SourceResult<Vec<Bytes>>>()?; .collect::<SourceResult<Vec<Bytes>>>()?;

View File

@ -490,7 +490,7 @@ fn parse_syntaxes(
.0 .0
.iter() .iter()
.map(|path| { .map(|path| {
let id = vm.location().join(path).at(span)?; let id = vm.resolve_path(path).at(span)?;
vm.world().file(id).at(span) vm.world().file(id).at(span)
}) })
.collect::<SourceResult<Vec<Bytes>>>()?; .collect::<SourceResult<Vec<Bytes>>>()?;
@ -522,7 +522,7 @@ fn parse_theme(
}; };
// Load theme file. // Load theme file.
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
// Check that parsing works. // Check that parsing works.

View File

@ -44,7 +44,7 @@ pub struct ImageElem {
#[parse( #[parse(
let Spanned { v: path, span } = let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?; args.expect::<Spanned<EcoString>>("path to image file")?;
let id = vm.location().join(&path).at(span)?; let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?; let data = vm.world().file(id).at(span)?;
path path
)] )]

View File

@ -16,9 +16,6 @@ use super::is_ident;
static INTERNER: Lazy<RwLock<Interner>> = static INTERNER: Lazy<RwLock<Interner>> =
Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() })); Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() }));
/// The path that we use for detached file ids.
static DETACHED_PATH: Lazy<VirtualPath> = Lazy::new(|| VirtualPath::new("/unknown"));
/// A package-path interner. /// A package-path interner.
struct Interner { struct Interner {
to_id: HashMap<Pair, FileId>, to_id: HashMap<Pair, FileId>,
@ -48,66 +45,41 @@ impl FileId {
} }
let mut interner = INTERNER.write().unwrap(); let mut interner = INTERNER.write().unwrap();
let len = interner.from_id.len(); let num = interner.from_id.len().try_into().expect("out of file ids");
if len >= usize::from(u16::MAX) {
panic!("too many file specifications");
}
// Create a new entry forever by leaking the pair. We can't leak more // Create a new entry forever by leaking the pair. We can't leak more
// than 2^16 pair (and typically will leak a lot less), so its not a // than 2^16 pair (and typically will leak a lot less), so its not a
// big deal. // big deal.
let id = FileId(len as u16); let id = FileId(num);
let leaked = Box::leak(Box::new(pair)); let leaked = Box::leak(Box::new(pair));
interner.to_id.insert(leaked, id); interner.to_id.insert(leaked, id);
interner.from_id.push(leaked); interner.from_id.push(leaked);
id id
} }
/// Get an id that does not identify any real file.
pub const fn detached() -> Self {
Self(u16::MAX)
}
/// Whether the id is the detached.
pub const fn is_detached(self) -> bool {
self.0 == Self::detached().0
}
/// The package the file resides in, if any. /// The package the file resides in, if any.
pub fn package(&self) -> Option<&'static PackageSpec> { pub fn package(&self) -> Option<&'static PackageSpec> {
if self.is_detached() { self.pair().0.as_ref()
None
} else {
self.pair().0.as_ref()
}
} }
/// The absolute and normalized path to the file _within_ the project or /// The absolute and normalized path to the file _within_ the project or
/// package. /// package.
pub fn vpath(&self) -> &'static VirtualPath { pub fn vpath(&self) -> &'static VirtualPath {
if self.is_detached() { &self.pair().1
&DETACHED_PATH
} else {
&self.pair().1
}
} }
/// Resolve a file location relative to this file. /// Resolve a file location relative to this file.
pub fn join(self, path: &str) -> Result<Self, EcoString> { pub fn join(self, path: &str) -> Self {
if self.is_detached() { Self::new(self.package().cloned(), self.vpath().join(path))
Err("cannot access file system from here")?;
}
Ok(Self::new(self.package().cloned(), self.vpath().join(path)))
} }
/// Construct from a raw number. /// Construct from a raw number.
pub(crate) const fn from_u16(v: u16) -> Self { pub(crate) const fn from_raw(v: u16) -> Self {
Self(v) Self(v)
} }
/// Extract the raw underlying number. /// Extract the raw underlying number.
pub(crate) const fn as_u16(self) -> u16 { pub(crate) const fn into_raw(self) -> u16 {
self.0 self.0
} }

View File

@ -202,7 +202,7 @@ impl SyntaxNode {
return Err(Unnumberable); return Err(Unnumberable);
} }
let mid = Span::new(id, (within.start + within.end) / 2); let mid = Span::new(id, (within.start + within.end) / 2).unwrap();
match &mut self.0 { match &mut self.0 {
Repr::Leaf(leaf) => leaf.span = mid, Repr::Leaf(leaf) => leaf.span = mid,
Repr::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within)?, Repr::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within)?,
@ -424,7 +424,7 @@ impl InnerNode {
let mut start = within.start; let mut start = within.start;
if range.is_none() { if range.is_none() {
let end = start + stride; let end = start + stride;
self.span = Span::new(id, (start + end) / 2); self.span = Span::new(id, (start + end) / 2).unwrap();
self.upper = within.end; self.upper = within.end;
start = end; start = end;
} }
@ -448,6 +448,7 @@ impl InnerNode {
mut range: Range<usize>, mut range: Range<usize>,
replacement: Vec<SyntaxNode>, replacement: Vec<SyntaxNode>,
) -> NumberingResult { ) -> NumberingResult {
let Some(id) = self.span.id() else { return Err(Unnumberable) };
let superseded = &self.children[range.clone()]; let superseded = &self.children[range.clone()];
// Compute the new byte length. // Compute the new byte length.
@ -505,7 +506,6 @@ impl InnerNode {
// Try to renumber. // Try to renumber.
let within = start_number..end_number; let within = start_number..end_number;
let id = self.span.id();
if self.numberize(id, Some(renumber), within).is_ok() { if self.numberize(id, Some(renumber), within).is_ok() {
return Ok(()); return Ok(());
} }

View File

@ -21,7 +21,9 @@ pub fn reparse(
try_reparse(text, replaced, replacement_len, None, root, 0).unwrap_or_else(|| { try_reparse(text, replaced, replacement_len, None, root, 0).unwrap_or_else(|| {
let id = root.span().id(); let id = root.span().id();
*root = parse(text); *root = parse(text);
root.numberize(id, Span::FULL).unwrap(); if let Some(id) = id {
root.numberize(id, Span::FULL).unwrap();
}
0..text.len() 0..text.len()
}) })
} }

View File

@ -9,6 +9,7 @@ use comemo::Prehashed;
use super::reparser::reparse; use super::reparser::reparse;
use super::{is_newline, parse, FileId, LinkedNode, Span, SyntaxNode}; use super::{is_newline, parse, FileId, LinkedNode, Span, SyntaxNode};
use crate::VirtualPath;
/// A source file. /// A source file.
/// ///
@ -44,19 +45,7 @@ impl Source {
/// Create a source file without a real id and path, usually for testing. /// Create a source file without a real id and path, usually for testing.
pub fn detached(text: impl Into<String>) -> Self { pub fn detached(text: impl Into<String>) -> Self {
Self::new(FileId::detached(), text.into()) Self::new(FileId::new(None, VirtualPath::new("main.typ")), text.into())
}
/// Create a source file with the same synthetic span for all nodes.
pub fn synthesized(text: String, span: Span) -> Self {
let mut root = parse(&text);
root.synthesize(span);
Self(Arc::new(Repr {
id: FileId::detached(),
lines: lines(&text),
text: Prehashed::new(text),
root: Prehashed::new(root),
}))
} }
/// The root node of the file's untyped syntax tree. /// The root node of the file's untyped syntax tree.
@ -151,12 +140,9 @@ impl Source {
/// Get the byte range for the given span in this file. /// Get the byte range for the given span in this file.
/// ///
/// Panics if the span does not point into this source file. /// Returns `None` if the span does not point into this source file.
#[track_caller] pub fn range(&self, span: Span) -> Option<Range<usize>> {
pub fn range(&self, span: Span) -> Range<usize> { Some(self.find(span)?.range())
self.find(span)
.expect("span does not point into this source file")
.range()
} }
/// Return the index of the UTF-16 code unit at the byte index. /// Return the index of the UTF-16 code unit at the byte index.

View File

@ -28,54 +28,57 @@ pub struct Span(NonZeroU64);
impl Span { impl Span {
/// The full range of numbers available for span numbering. /// The full range of numbers available for span numbering.
pub const FULL: Range<u64> = 2..(1 << Self::BITS); pub(super) const FULL: Range<u64> = 2..(1 << Self::BITS);
/// The value reserved for the detached span.
const DETACHED: u64 = 1; const DETACHED: u64 = 1;
// Data layout: /// Data layout:
// | 16 bits source id | 48 bits number | /// | 16 bits source id | 48 bits number |
const BITS: usize = 48; const BITS: usize = 48;
/// Create a new span from a source id and a unique number. /// Create a new span from a source id and a unique number.
/// ///
/// Panics if the `number` is not contained in `FULL`. /// Returns `None` if `number` is not contained in `FULL`.
#[track_caller] pub(super) const fn new(id: FileId, number: u64) -> Option<Self> {
pub const fn new(id: FileId, number: u64) -> Self { if number < Self::FULL.start || number >= Self::FULL.end {
assert!( return None;
Self::FULL.start <= number && number < Self::FULL.end, }
"span number outside valid range"
);
Self::pack(id, number) let bits = ((id.into_raw() as u64) << Self::BITS) | number;
}
/// A span that does not point into any source file.
pub const fn detached() -> Self {
Self::pack(FileId::detached(), Self::DETACHED)
}
/// Pack the components into a span.
#[track_caller]
const fn pack(id: FileId, number: u64) -> Span {
let bits = ((id.as_u16() as u64) << Self::BITS) | number;
match NonZeroU64::new(bits) { match NonZeroU64::new(bits) {
Some(v) => Self(v), Some(v) => Some(Self(v)),
None => panic!("span encoding is zero"), None => unreachable!(),
} }
} }
/// The id of the source file the span points into. /// Create a span that does not point into any source file.
pub const fn id(self) -> FileId { pub const fn detached() -> Self {
FileId::from_u16((self.0.get() >> Self::BITS) as u16) match NonZeroU64::new(Self::DETACHED) {
} Some(v) => Self(v),
None => unreachable!(),
/// The unique number of the span within its source file. }
pub const fn number(self) -> u64 {
self.0.get() & ((1 << Self::BITS) - 1)
} }
/// Whether the span is detached. /// Whether the span is detached.
pub const fn is_detached(self) -> bool { pub const fn is_detached(self) -> bool {
self.id().is_detached() self.0.get() == Self::DETACHED
}
/// The id of the source file the span points into.
///
/// Returns `None` if the span is detached.
pub const fn id(self) -> Option<FileId> {
if self.is_detached() {
return None;
}
let bits = (self.0.get() >> Self::BITS) as u16;
Some(FileId::from_raw(bits))
}
/// The unique number of the span within its [`Source`](super::Source).
pub const fn number(self) -> u64 {
self.0.get() & ((1 << Self::BITS) - 1)
} }
} }
@ -120,9 +123,9 @@ mod tests {
#[test] #[test]
fn test_span_encoding() { fn test_span_encoding() {
let id = FileId::from_u16(5); let id = FileId::from_raw(5);
let span = Span::new(id, 10); let span = Span::new(id, 10).unwrap();
assert_eq!(span.id(), id); assert_eq!(span.id(), Some(id));
assert_eq!(span.number(), 10); assert_eq!(span.number(), 10);
} }
} }

View File

@ -9,7 +9,7 @@ use std::string::FromUtf8Error;
use comemo::Tracked; use comemo::Tracked;
use crate::syntax::{PackageSpec, Span, Spanned, SyntaxError}; use crate::syntax::{PackageSpec, Span, Spanned, SyntaxError};
use crate::World; use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`]. /// Early-return with a [`StrResult`] or [`SourceResult`].
/// ///
@ -206,16 +206,12 @@ impl<T> Trace<T> for SourceResult<T> {
F: Fn() -> Tracepoint, F: Fn() -> Tracepoint,
{ {
self.map_err(|mut errors| { self.map_err(|mut errors| {
if span.is_detached() { let Some(trace_range) = world.range(span) else { return errors };
return errors; for error in errors.iter_mut() {
}
let trace_range = world.range(span);
for error in errors.iter_mut().filter(|e| !e.span.is_detached()) {
// Skip traces that surround the error. // Skip traces that surround the error.
if error.span.id() == span.id() { if let Some(error_range) = world.range(error.span) {
let error_range = world.range(error.span); if error.span.id() == span.id()
if trace_range.start <= error_range.start && trace_range.start <= error_range.start
&& trace_range.end >= error_range.end && trace_range.end >= error_range.end
{ {
continue; continue;

View File

@ -94,9 +94,8 @@ impl Func {
} }
Repr::Closure(closure) => { Repr::Closure(closure) => {
// Determine the route inside the closure. // Determine the route inside the closure.
let fresh = Route::new(closure.location); let fresh = Route::new(closure.file);
let route = let route = if vm.file.is_none() { fresh.track() } else { vm.route };
if vm.location.is_detached() { fresh.track() } else { vm.route };
Closure::call( Closure::call(
self, self,
@ -134,7 +133,7 @@ impl Func {
delayed: TrackedMut::reborrow_mut(&mut vt.delayed), delayed: TrackedMut::reborrow_mut(&mut vt.delayed),
tracer: TrackedMut::reborrow_mut(&mut vt.tracer), tracer: TrackedMut::reborrow_mut(&mut vt.tracer),
}; };
let mut vm = Vm::new(vt, route.track(), FileId::detached(), scopes); let mut vm = Vm::new(vt, route.track(), None, scopes);
let args = Args::new(self.span(), args); let args = Args::new(self.span(), args);
self.call_vm(&mut vm, args) self.call_vm(&mut vm, args)
} }
@ -298,7 +297,7 @@ pub(super) struct Closure {
/// The closure's syntax node. Must be castable to `ast::Closure`. /// The closure's syntax node. Must be castable to `ast::Closure`.
pub node: SyntaxNode, pub node: SyntaxNode,
/// The source file where the closure was defined. /// The source file where the closure was defined.
pub location: FileId, pub file: Option<FileId>,
/// Default values of named parameters. /// Default values of named parameters.
pub defaults: Vec<Value>, pub defaults: Vec<Value>,
/// Captured values from outer scopes. /// Captured values from outer scopes.
@ -351,7 +350,7 @@ impl Closure {
}; };
// Prepare VM. // Prepare VM.
let mut vm = Vm::new(vt, route, this.location, scopes); let mut vm = Vm::new(vt, route, this.file, scopes);
vm.depth = depth; vm.depth = depth;
// Provide the closure itself for recursive calls. // Provide the closure itself for recursive calls.

View File

@ -122,7 +122,7 @@ pub fn eval(
// Prepare VM. // Prepare VM.
let route = Route::insert(route, id); let route = Route::insert(route, id);
let scopes = Scopes::new(Some(library)); let scopes = Scopes::new(Some(library));
let mut vm = Vm::new(vt, route.track(), id, scopes); let mut vm = Vm::new(vt, route.track(), Some(id), scopes);
let root = source.root(); let root = source.root();
let errors = root.errors(); let errors = root.errors();
@ -189,9 +189,8 @@ pub fn eval_string(
// Prepare VM. // Prepare VM.
let route = Route::default(); let route = Route::default();
let id = FileId::detached();
let scopes = Scopes::new(Some(world.library())); let scopes = Scopes::new(Some(world.library()));
let mut vm = Vm::new(vt, route.track(), id, scopes); let mut vm = Vm::new(vt, route.track(), None, scopes);
vm.scopes.scopes.push(scope); vm.scopes.scopes.push(scope);
// Evaluate the code. // Evaluate the code.
@ -235,8 +234,8 @@ pub struct Vm<'a> {
items: LangItems, items: LangItems,
/// The route of source ids the VM took to reach its current location. /// The route of source ids the VM took to reach its current location.
route: Tracked<'a, Route<'a>>, route: Tracked<'a, Route<'a>>,
/// The current location. /// The id of the currently evaluated file.
location: FileId, file: Option<FileId>,
/// A control flow event that is currently happening. /// A control flow event that is currently happening.
flow: Option<FlowEvent>, flow: Option<FlowEvent>,
/// The stack of scopes. /// The stack of scopes.
@ -252,16 +251,16 @@ impl<'a> Vm<'a> {
fn new( fn new(
vt: Vt<'a>, vt: Vt<'a>,
route: Tracked<'a, Route>, route: Tracked<'a, Route>,
location: FileId, file: Option<FileId>,
scopes: Scopes<'a>, scopes: Scopes<'a>,
) -> Self { ) -> Self {
let traced = vt.tracer.span(location); let traced = file.and_then(|id| vt.tracer.span(id));
let items = vt.world.library().items.clone(); let items = vt.world.library().items.clone();
Self { Self {
vt, vt,
items, items,
route, route,
location, file,
flow: None, flow: None,
scopes, scopes,
depth: 0, depth: 0,
@ -274,9 +273,21 @@ impl<'a> Vm<'a> {
self.vt.world self.vt.world
} }
/// The location to which paths are relative currently. /// The id of the currently evaluated file.
pub fn location(&self) -> FileId { ///
self.location /// Returns `None` if the VM is in a detached context, e.g. when evaluating
/// a user-provided string.
pub fn file(&self) -> Option<FileId> {
self.file
}
/// Resolve a path relative to the currently evaluated file.
pub fn resolve_path(&self, path: &str) -> StrResult<FileId> {
let Some(file) = self.file else {
bail!("cannot access file system from here");
};
Ok(file.join(path))
} }
/// Define a variable in the current scope. /// Define a variable in the current scope.
@ -331,8 +342,8 @@ pub struct Route<'a> {
impl<'a> Route<'a> { impl<'a> Route<'a> {
/// Create a new route with just one entry. /// Create a new route with just one entry.
pub fn new(id: FileId) -> Self { pub fn new(id: Option<FileId>) -> Self {
Self { id: Some(id), outer: None } Self { id, outer: None }
} }
/// Insert a new id into the route. /// Insert a new id into the route.
@ -1301,7 +1312,7 @@ impl Eval for ast::Closure<'_> {
// Define the closure. // Define the closure.
let closure = Closure { let closure = Closure {
node: self.to_untyped().clone(), node: self.to_untyped().clone(),
location: vm.location, file: vm.file,
defaults, defaults,
captured, captured,
}; };
@ -1809,7 +1820,7 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
manifest.validate(&spec).at(span)?; manifest.validate(&spec).at(span)?;
// Evaluate the entry point. // Evaluate the entry point.
let entrypoint_id = manifest_id.join(&manifest.package.entrypoint).at(span)?; let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
let source = vm.world().source(entrypoint_id).at(span)?; let source = vm.world().source(entrypoint_id).at(span)?;
let point = || Tracepoint::Import; let point = || Tracepoint::Import;
Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source)
@ -1821,7 +1832,7 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
// Load the source file. // Load the source file.
let world = vm.world(); let world = vm.world();
let id = vm.location().join(path).at(span)?; let id = vm.resolve_path(path).at(span)?;
let source = world.source(id).at(span)?; let source = world.source(id).at(span)?;
// Prevent cyclic importing. // Prevent cyclic importing.

View File

@ -45,7 +45,7 @@ impl Tracer {
impl Tracer { impl Tracer {
/// The traced span if it is part of the given source file. /// The traced span if it is part of the given source file.
pub fn span(&self, id: FileId) -> Option<Span> { pub fn span(&self, id: FileId) -> Option<Span> {
if self.span.map(Span::id) == Some(id) { if self.span.and_then(Span::id) == Some(id) {
self.span self.span
} else { } else {
None None

View File

@ -46,7 +46,7 @@ pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> {
pub fn analyze_import(world: &dyn World, source: &Source, path: &str) -> Option<Module> { pub fn analyze_import(world: &dyn World, source: &Source, path: &str) -> Option<Module> {
let route = Route::default(); let route = Route::default();
let mut tracer = Tracer::default(); let mut tracer = Tracer::default();
let id = source.id().join(path).ok()?; let id = source.id().join(path);
let source = world.source(id).ok()?; let source = world.source(id).ok()?;
eval(world.track(), route.track(), tracer.track_mut(), &source).ok() eval(world.track(), route.track(), tracer.track_mut(), &source).ok()
} }

View File

@ -379,7 +379,7 @@ mod tests {
use std::ops::Range; use std::ops::Range;
use super::*; use super::*;
use crate::syntax::Source; use crate::syntax::parse;
#[test] #[test]
fn test_highlighting() { fn test_highlighting() {
@ -388,8 +388,8 @@ mod tests {
#[track_caller] #[track_caller]
fn test(text: &str, goal: &[(Range<usize>, Tag)]) { fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
let mut vec = vec![]; let mut vec = vec![];
let source = Source::detached(text); let root = parse(text);
highlight_tree(&mut vec, &LinkedNode::new(source.root())); highlight_tree(&mut vec, &LinkedNode::new(&root));
assert_eq!(vec, goal); assert_eq!(vec, goal);
} }

View File

@ -21,9 +21,10 @@ pub enum Jump {
impl Jump { impl Jump {
fn from_span(world: &dyn World, span: Span) -> Option<Self> { fn from_span(world: &dyn World, span: Span) -> Option<Self> {
let source = world.source(span.id()).ok()?; let id = span.id()?;
let source = world.source(id).ok()?;
let node = source.find(span)?; let node = source.find(span)?;
Some(Self::Source(span.id(), node.offset())) Some(Self::Source(id, node.offset()))
} }
} }
@ -67,18 +68,15 @@ pub fn jump_from_click(
FrameItem::Text(text) => { FrameItem::Text(text) => {
for glyph in &text.glyphs { for glyph in &text.glyphs {
let (span, span_offset) = glyph.span;
if span.is_detached() {
continue;
}
let width = glyph.x_advance.at(text.size); let width = glyph.x_advance.at(text.size);
if is_in_rect( if is_in_rect(
Point::new(pos.x, pos.y - text.size), Point::new(pos.x, pos.y - text.size),
Size::new(width, text.size), Size::new(width, text.size),
click, click,
) { ) {
let source = world.source(span.id()).ok()?; let (span, span_offset) = glyph.span;
let Some(id) = span.id() else { continue };
let source = world.source(id).ok()?;
let node = source.find(span)?; let node = source.find(span)?;
let pos = if node.kind() == SyntaxKind::Text { let pos = if node.kind() == SyntaxKind::Text {
let range = node.range(); let range = node.range();

View File

@ -142,12 +142,18 @@ pub trait World {
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
&[] &[]
} }
}
/// Helper methods on [`World`] implementations.
pub trait WorldExt {
/// Get the byte range for a span. /// Get the byte range for a span.
#[track_caller] ///
fn range(&self, span: Span) -> Range<usize> { /// Returns `None` if the `Span` does not point into any source file.
self.source(span.id()) fn range(&self, span: Span) -> Option<Range<usize>>;
.expect("span does not point into any source file") }
.range(span)
impl<T: World> WorldExt for T {
fn range(&self, span: Span) -> Option<Range<usize>> {
self.source(span.id()?).ok()?.range(span)
} }
} }

View File

@ -31,6 +31,7 @@ use comemo::{Track, Tracked, TrackedMut, Validate};
use crate::diag::{warning, SourceDiagnostic, SourceResult}; use crate::diag::{warning, SourceDiagnostic, SourceResult};
use crate::doc::Document; use crate::doc::Document;
use crate::eval::Tracer; use crate::eval::Tracer;
use crate::syntax::Span;
use crate::World; use crate::World;
/// Typeset content into a fully layouted document. /// Typeset content into a fully layouted document.
@ -88,11 +89,8 @@ pub fn typeset(
if iter >= 5 { if iter >= 5 {
tracer.warn( tracer.warn(
warning!( warning!(Span::detached(), "layout did not converge within 5 attempts",)
world.main().root().span(), .with_hint("check if any states or queries are updating themselves"),
"layout did not converge within 5 attempts",
)
.with_hint("check if any states or queries are updating themselves"),
); );
break; break;
} }

View File

@ -10,6 +10,7 @@ publish = false
typst = { path = "../crates/typst" } typst = { path = "../crates/typst" }
typst-library = { path = "../crates/typst-library" } typst-library = { path = "../crates/typst-library" }
comemo = "0.3" comemo = "0.3"
ecow = { version = "0.1.1", features = ["serde"] }
iai = { git = "https://github.com/reknih/iai" } iai = { git = "https://github.com/reknih/iai" }
once_cell = "1" once_cell = "1"
oxipng = { version = "8.0.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } oxipng = { version = "8.0.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }

View File

@ -4,15 +4,15 @@ use std::cell::{RefCell, RefMut};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Write as FmtWrite; use std::fmt::{self, Display, Formatter, Write as _};
use std::fs; use std::fs;
use std::io::{self, Write}; use std::io::{self, Write};
use std::iter;
use std::ops::Range; use std::ops::Range;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use comemo::{Prehashed, Track}; use comemo::{Prehashed, Track};
use ecow::EcoString;
use oxipng::{InFile, Options, OutFile}; use oxipng::{InFile, Options, OutFile};
use rayon::iter::{ParallelBridge, ParallelIterator}; use rayon::iter::{ParallelBridge, ParallelIterator};
use std::cell::OnceCell; use std::cell::OnceCell;
@ -26,7 +26,7 @@ use typst::eval::{eco_format, func, Bytes, Datetime, Library, NoneValue, Tracer,
use typst::font::{Font, FontBook}; use typst::font::{Font, FontBook};
use typst::geom::{Abs, Color, RgbaColor, Smart}; use typst::geom::{Abs, Color, RgbaColor, Smart};
use typst::syntax::{FileId, Source, Span, SyntaxNode, VirtualPath}; use typst::syntax::{FileId, Source, Span, SyntaxNode, VirtualPath};
use typst::World; use typst::{World, WorldExt};
use typst_library::layout::{Margin, PageElem}; use typst_library::layout::{Margin, PageElem};
use typst_library::text::{TextElem, TextSize}; use typst_library::text::{TextElem, TextSize};
@ -237,7 +237,7 @@ impl TestWorld {
Self { Self {
print, print,
main: FileId::detached(), main: FileId::new(None, VirtualPath::new("main.typ")),
library: Prehashed::new(library()), library: Prehashed::new(library()),
book: Prehashed::new(FontBook::from_fonts(&fonts)), book: Prehashed::new(FontBook::from_fonts(&fonts)),
fonts, fonts,
@ -555,39 +555,46 @@ fn test_part(
// This has one caveat: due to the format of the expected hints, we can not // This has one caveat: due to the format of the expected hints, we can not
// verify if a hint belongs to a diagnostic or not. That should be irrelevant // verify if a hint belongs to a diagnostic or not. That should be irrelevant
// however, as the line of the hint is still verified. // however, as the line of the hint is still verified.
let actual_diagnostics: HashSet<UserOutput> = diagnostics let mut actual_diagnostics = HashSet::new();
.into_iter() for diagnostic in &diagnostics {
.inspect(|diagnostic| assert!(!diagnostic.span.is_detached())) // Ignore diagnostics from other files.
.filter(|diagnostic| diagnostic.span.id() == source.id()) if diagnostic.span.id().map_or(false, |id| id != source.id()) {
.flat_map(|diagnostic| { continue;
let range = world.range(diagnostic.span); }
let message = diagnostic.message.replace('\\', "/");
let output = match diagnostic.severity {
Severity::Error => UserOutput::Error(range.clone(), message),
Severity::Warning => UserOutput::Warning(range.clone(), message),
};
let hints = diagnostic let annotation = Annotation {
.hints kind: match diagnostic.severity {
.iter() Severity::Error => AnnotationKind::Error,
.filter(|_| validate_hints) // No unexpected hints should be verified if disabled. Severity::Warning => AnnotationKind::Warning,
.map(|hint| UserOutput::Hint(range.clone(), hint.to_string())); },
range: world.range(diagnostic.span),
message: diagnostic.message.replace('\\', "/").into(),
};
iter::once(output).chain(hints).collect::<Vec<_>>() if validate_hints {
}) for hint in &diagnostic.hints {
.collect(); actual_diagnostics.insert(Annotation {
kind: AnnotationKind::Hint,
message: hint.clone(),
range: annotation.range.clone(),
});
}
}
actual_diagnostics.insert(annotation);
}
// Basically symmetric_difference, but we need to know where an item is coming from. // Basically symmetric_difference, but we need to know where an item is coming from.
let mut unexpected_outputs = actual_diagnostics let mut unexpected_outputs = actual_diagnostics
.difference(&metadata.invariants) .difference(&metadata.annotations)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut missing_outputs = metadata let mut missing_outputs = metadata
.invariants .annotations
.difference(&actual_diagnostics) .difference(&actual_diagnostics)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
unexpected_outputs.sort_by_key(|&o| o.start()); unexpected_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));
missing_outputs.sort_by_key(|&o| o.start()); missing_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));
// This prints all unexpected emits first, then all missing emits. // This prints all unexpected emits first, then all missing emits.
// Is this reasonable or subject to change? // Is this reasonable or subject to change?
@ -597,41 +604,34 @@ fn test_part(
for unexpected in unexpected_outputs { for unexpected in unexpected_outputs {
write!(output, " Not annotated | ").unwrap(); write!(output, " Not annotated | ").unwrap();
print_user_output(output, &source, line, unexpected) print_annotation(output, &source, line, unexpected)
} }
for missing in missing_outputs { for missing in missing_outputs {
write!(output, " Not emitted | ").unwrap(); write!(output, " Not emitted | ").unwrap();
print_user_output(output, &source, line, missing) print_annotation(output, &source, line, missing)
} }
} }
(ok, compare_ref, frames) (ok, compare_ref, frames)
} }
fn print_user_output( fn print_annotation(
output: &mut String, output: &mut String,
source: &Source, source: &Source,
line: usize, line: usize,
user_output: &UserOutput, annotation: &Annotation,
) { ) {
let (range, message) = match &user_output { let Annotation { range, message, kind } = annotation;
UserOutput::Error(r, m) => (r, m), write!(output, "{kind}: ").unwrap();
UserOutput::Warning(r, m) => (r, m), if let Some(range) = range {
UserOutput::Hint(r, m) => (r, m), let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
}; let start_col = 1 + source.byte_to_column(range.start).unwrap();
let end_line = 1 + line + source.byte_to_line(range.end).unwrap();
let start_line = 1 + line + source.byte_to_line(range.start).unwrap(); let end_col = 1 + source.byte_to_column(range.end).unwrap();
let start_col = 1 + source.byte_to_column(range.start).unwrap(); write!(output, "{start_line}:{start_col}-{end_line}:{end_col}: ").unwrap();
let end_line = 1 + line + source.byte_to_line(range.end).unwrap(); }
let end_col = 1 + source.byte_to_column(range.end).unwrap(); writeln!(output, "{message}").unwrap();
let kind = match user_output {
UserOutput::Error(_, _) => "Error",
UserOutput::Warning(_, _) => "Warning",
UserOutput::Hint(_, _) => "Hint",
};
writeln!(output, "{kind}: {start_line}:{start_col}-{end_line}:{end_col}: {message}")
.unwrap();
} }
struct TestConfiguration { struct TestConfiguration {
@ -641,101 +641,93 @@ struct TestConfiguration {
struct TestPartMetadata { struct TestPartMetadata {
part_configuration: TestConfiguration, part_configuration: TestConfiguration,
invariants: HashSet<UserOutput>, annotations: HashSet<Annotation>,
} }
#[derive(PartialEq, Eq, Debug, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum UserOutput { struct Annotation {
Error(Range<usize>, String), range: Option<Range<usize>>,
Warning(Range<usize>, String), message: EcoString,
Hint(Range<usize>, String), kind: AnnotationKind,
} }
impl UserOutput { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
fn start(&self) -> usize { enum AnnotationKind {
Error,
Warning,
Hint,
}
impl AnnotationKind {
fn iter() -> impl Iterator<Item = Self> {
[AnnotationKind::Error, AnnotationKind::Warning, AnnotationKind::Hint].into_iter()
}
fn as_str(self) -> &'static str {
match self { match self {
UserOutput::Error(r, _) => r.start, AnnotationKind::Error => "Error",
UserOutput::Warning(r, _) => r.start, AnnotationKind::Warning => "Warning",
UserOutput::Hint(r, _) => r.start, AnnotationKind::Hint => "Hint",
} }
} }
}
fn error(range: Range<usize>, message: String) -> UserOutput { impl Display for AnnotationKind {
UserOutput::Error(range, message) fn fmt(&self, f: &mut Formatter) -> fmt::Result {
} f.pad(self.as_str())
fn warning(range: Range<usize>, message: String) -> UserOutput {
UserOutput::Warning(range, message)
}
fn hint(range: Range<usize>, message: String) -> UserOutput {
UserOutput::Hint(range, message)
} }
} }
fn parse_part_metadata(source: &Source) -> TestPartMetadata { fn parse_part_metadata(source: &Source) -> TestPartMetadata {
let mut compare_ref = None; let mut compare_ref = None;
let mut validate_hints = None; let mut validate_hints = None;
let mut expectations = HashSet::default(); let mut annotations = HashSet::default();
let lines: Vec<_> = source.text().lines().map(str::trim).collect(); let lines: Vec<_> = source.text().lines().map(str::trim).collect();
for (i, line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
compare_ref = get_flag_metadata(line, "Ref").or(compare_ref); compare_ref = get_flag_metadata(line, "Ref").or(compare_ref);
validate_hints = get_flag_metadata(line, "Hints").or(validate_hints); validate_hints = get_flag_metadata(line, "Hints").or(validate_hints);
fn num(s: &mut Scanner) -> isize { fn num(s: &mut Scanner) -> Option<isize> {
let mut first = true; let mut first = true;
let n = &s.eat_while(|c: char| { let n = &s.eat_while(|c: char| {
let valid = first && c == '-' || c.is_numeric(); let valid = first && c == '-' || c.is_numeric();
first = false; first = false;
valid valid
}); });
n.parse().unwrap_or_else(|e| panic!("{n} is not a number ({e})")) n.parse().ok()
} }
let comments_until_code = let comments_until_code =
lines[i..].iter().take_while(|line| line.starts_with("//")).count(); lines[i..].iter().take_while(|line| line.starts_with("//")).count();
let pos = |s: &mut Scanner| -> usize { let pos = |s: &mut Scanner| -> Option<usize> {
let first = num(s) - 1; let first = num(s)? - 1;
let (delta, column) = let (delta, column) =
if s.eat_if(':') { (first, num(s) - 1) } else { (0, first) }; if s.eat_if(':') { (first, num(s)? - 1) } else { (0, first) };
let line = (i + comments_until_code) let line = (i + comments_until_code).checked_add_signed(delta)?;
.checked_add_signed(delta) source.line_column_to_byte(line, usize::try_from(column).ok()?)
.expect("line number overflowed limits");
source
.line_column_to_byte(
line,
usize::try_from(column).expect("column number overflowed limits"),
)
.unwrap()
}; };
let error_factory: fn(Range<usize>, String) -> UserOutput = UserOutput::error; let range = |s: &mut Scanner| -> Option<Range<usize>> {
let warning_factory: fn(Range<usize>, String) -> UserOutput = UserOutput::warning; let start = pos(s)?;
let hint_factory: fn(Range<usize>, String) -> UserOutput = UserOutput::hint; let end = if s.eat_if('-') { pos(s)? } else { start };
Some(start..end)
};
let error_metadata = get_metadata(line, "Error").map(|s| (s, error_factory)); for kind in AnnotationKind::iter() {
let get_warning_metadata = let Some(expectation) = get_metadata(line, kind.as_str()) else { continue };
|| get_metadata(line, "Warning").map(|s| (s, warning_factory));
let get_hint_metadata = || get_metadata(line, "Hint").map(|s| (s, hint_factory));
if let Some((expectation, factory)) = error_metadata
.or_else(get_warning_metadata)
.or_else(get_hint_metadata)
{
let mut s = Scanner::new(expectation); let mut s = Scanner::new(expectation);
let start = pos(&mut s); let range = range(&mut s);
let end = if s.eat_if('-') { pos(&mut s) } else { start }; let rest = if range.is_some() { s.after() } else { s.string() };
let range = start..end; let message = rest.trim().into();
annotations.insert(Annotation { kind, range, message });
expectations.insert(factory(range, s.after().trim().to_string())); }
};
} }
TestPartMetadata { TestPartMetadata {
part_configuration: TestConfiguration { compare_ref, validate_hints }, part_configuration: TestConfiguration { compare_ref, validate_hints },
invariants: expectations, annotations,
} }
} }

View File

@ -49,10 +49,8 @@ Was: #locate(location => {
--- ---
// Make sure that a warning is produced if the layout fails to converge. // Make sure that a warning is produced if the layout fails to converge.
// Warning: -3:1-6:1 layout did not converge within 5 attempts // Warning: layout did not converge within 5 attempts
// Hint: -3:1-6:1 check if any states or queries are updating themselves // Hint: check if any states or queries are updating themselves
#let s = state("x", 1) #let s = state("s", 1)
#locate(loc => { #locate(loc => s.update(s.final(loc) + 1))
s.update(s.final(loc) + 1)
})
#s.display() #s.display()