mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +08:00
More type safety for spans
This commit is contained in:
parent
7bdf1f57b0
commit
a71a2057f2
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2990,6 +2990,7 @@ version = "0.7.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"comemo",
|
"comemo",
|
||||||
|
"ecow",
|
||||||
"iai",
|
"iai",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"oxipng",
|
"oxipng",
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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>>>()?;
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
)]
|
)]
|
||||||
|
@ -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() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
self.pair().0.as_ref()
|
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() {
|
|
||||||
&DETACHED_PATH
|
|
||||||
} else {
|
|
||||||
&self.pair().1
|
&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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(());
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
if let Some(id) = id {
|
||||||
root.numberize(id, Span::FULL).unwrap();
|
root.numberize(id, Span::FULL).unwrap();
|
||||||
|
}
|
||||||
0..text.len()
|
0..text.len()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A span that does not point into any source file.
|
let bits = ((id.into_raw() as u64) << Self::BITS) | number;
|
||||||
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) => Some(Self(v)),
|
||||||
|
None => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a span that does not point into any source file.
|
||||||
|
pub const fn detached() -> Self {
|
||||||
|
match NonZeroU64::new(Self::DETACHED) {
|
||||||
Some(v) => Self(v),
|
Some(v) => Self(v),
|
||||||
None => panic!("span encoding is zero"),
|
None => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The id of the source file the span points into.
|
|
||||||
pub const fn id(self) -> FileId {
|
|
||||||
FileId::from_u16((self.0.get() >> Self::BITS) as u16)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,10 +89,7 @@ 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(),
|
|
||||||
"layout did not converge within 5 attempts",
|
|
||||||
)
|
|
||||||
.with_hint("check if any states or queries are updating themselves"),
|
.with_hint("check if any states or queries are updating themselves"),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -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"] }
|
||||||
|
@ -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 {
|
let annotation = Annotation {
|
||||||
Severity::Error => UserOutput::Error(range.clone(), message),
|
kind: match diagnostic.severity {
|
||||||
Severity::Warning => UserOutput::Warning(range.clone(), message),
|
Severity::Error => AnnotationKind::Error,
|
||||||
|
Severity::Warning => AnnotationKind::Warning,
|
||||||
|
},
|
||||||
|
range: world.range(diagnostic.span),
|
||||||
|
message: diagnostic.message.replace('\\', "/").into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let hints = diagnostic
|
if validate_hints {
|
||||||
.hints
|
for hint in &diagnostic.hints {
|
||||||
.iter()
|
actual_diagnostics.insert(Annotation {
|
||||||
.filter(|_| validate_hints) // No unexpected hints should be verified if disabled.
|
kind: AnnotationKind::Hint,
|
||||||
.map(|hint| UserOutput::Hint(range.clone(), hint.to_string()));
|
message: hint.clone(),
|
||||||
|
range: annotation.range.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
iter::once(output).chain(hints).collect::<Vec<_>>()
|
actual_diagnostics.insert(annotation);
|
||||||
})
|
}
|
||||||
.collect();
|
|
||||||
|
|
||||||
// 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_line = 1 + line + source.byte_to_line(range.start).unwrap();
|
||||||
let start_col = 1 + source.byte_to_column(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 end_line = 1 + line + source.byte_to_line(range.end).unwrap();
|
||||||
let end_col = 1 + source.byte_to_column(range.end).unwrap();
|
let end_col = 1 + source.byte_to_column(range.end).unwrap();
|
||||||
let kind = match user_output {
|
write!(output, "{start_line}:{start_col}-{end_line}:{end_col}: ").unwrap();
|
||||||
UserOutput::Error(_, _) => "Error",
|
}
|
||||||
UserOutput::Warning(_, _) => "Warning",
|
writeln!(output, "{message}").unwrap();
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user