From 9bbebd69ddb4a7d7da98c3a79ff7d0cb187873fd Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 31 May 2022 12:37:05 +0200 Subject: [PATCH] Numbered spans --- src/diag.rs | 20 +++- src/library/text/raw.rs | 2 +- src/main.rs | 4 +- src/parse/incremental.rs | 4 +- src/parse/mod.rs | 3 +- src/parse/parser.rs | 3 +- src/parse/tokens.rs | 3 +- src/source.rs | 41 ++++++-- src/syntax/highlight.rs | 34 +++++-- src/syntax/mod.rs | 208 +++++++++++++++++++++++++++++---------- src/syntax/span.rs | 145 +++++++-------------------- tests/typeset.rs | 64 +++++++----- 12 files changed, 313 insertions(+), 218 deletions(-) diff --git a/src/diag.rs b/src/diag.rs index 9e756bfee..f9ffb1a59 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -36,8 +36,10 @@ pub type StrResult = Result; /// An error in a source file. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Error { - /// The erroneous location in the source code. + /// The erroneous node in the source code. pub span: Span, + /// Where in the node the error should be annotated. + pub pos: ErrorPos, /// A diagnostic message describing the problem. pub message: String, /// The trace of function calls leading to the error. @@ -49,12 +51,24 @@ impl Error { pub fn new(span: Span, message: impl Into) -> Self { Self { span, + pos: ErrorPos::Full, trace: vec![], message: message.into(), } } } +/// Where in a node an error should be annotated. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ErrorPos { + /// At the start of the node. + Start, + /// Over the full width of the node. + Full, + /// At the end of the node. + End, +} + /// A part of an error's [trace](Error::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum Tracepoint { @@ -110,9 +124,7 @@ impl Trace for TypResult { { self.map_err(|mut errors| { for error in errors.iter_mut() { - if !span.surrounds(error.span) { - error.trace.push(Spanned::new(make_point(), span)); - } + error.trace.push(Spanned::new(make_point(), span)); } errors }) diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index fe1f9a991..a24d21704 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -75,7 +75,7 @@ impl Show for RawNode { }; let mut seq = vec![]; - syntax::highlight_themed(&self.text, mode, &THEME, &mut |piece, style| { + syntax::highlight_themed(&self.text, mode, &THEME, |piece, style| { seq.push(styled(piece, foreground, style)); }); diff --git a/src/main.rs b/src/main.rs index 33302eee4..d518a03b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -240,7 +240,7 @@ fn print_diagnostics( for error in errors { // The main diagnostic. let diag = Diagnostic::error().with_message(error.message).with_labels(vec![ - Label::primary(error.span.source, error.span.to_range()), + Label::primary(error.span.source(), sources.range(error.span)), ]); term::emit(&mut w, &config, sources, &diag)?; @@ -249,7 +249,7 @@ fn print_diagnostics( for point in error.trace { let message = point.v.to_string(); let help = Diagnostic::help().with_message(message).with_labels(vec![ - Label::primary(point.span.source, point.span.to_range()), + Label::primary(point.span.source(), sources.range(point.span)), ]); term::emit(&mut w, &config, sources, &help)?; diff --git a/src/parse/incremental.rs b/src/parse/incremental.rs index d81a5a232..e567cb073 100644 --- a/src/parse/incremental.rs +++ b/src/parse/incremental.rs @@ -134,6 +134,7 @@ impl Reparser<'_> { if let SearchState::Contained(pos) = search { let child = &mut node.children_mut()[pos.idx]; let prev_len = child.len(); + let prev_count = child.count(); if let Some(range) = match child { SyntaxNode::Inner(node) => { @@ -142,7 +143,8 @@ impl Reparser<'_> { SyntaxNode::Leaf(_) => None, } { let new_len = child.len(); - node.update_parent(new_len, prev_len); + let new_count = child.count(); + node.update_parent(prev_len, new_len, prev_count, new_count); return Some(range); } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 98d6470cd..fddef04c1 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -11,8 +11,9 @@ pub use tokens::*; use std::collections::HashSet; +use crate::diag::ErrorPos; use crate::syntax::ast::{Associativity, BinOp, UnOp}; -use crate::syntax::{ErrorPos, NodeKind, SyntaxNode}; +use crate::syntax::{NodeKind, SyntaxNode}; use crate::util::EcoString; /// Parse a source file. diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 3d7cac451..722e53ceb 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -3,7 +3,8 @@ use std::mem; use std::ops::Range; use super::{TokenMode, Tokens}; -use crate::syntax::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode}; +use crate::diag::ErrorPos; +use crate::syntax::{InnerNode, NodeData, NodeKind, SyntaxNode}; use crate::util::EcoString; /// A convenient token-based parser. diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index f095bd095..921559093 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -4,9 +4,10 @@ use unicode_xid::UnicodeXID; use unscanny::Scanner; use super::resolve::{resolve_hex, resolve_raw, resolve_string}; +use crate::diag::ErrorPos; use crate::geom::{AngleUnit, LengthUnit}; use crate::syntax::ast::{MathNode, RawNode, Unit}; -use crate::syntax::{ErrorPos, NodeKind}; +use crate::syntax::NodeKind; use crate::util::EcoString; /// An iterator over the tokens of a string of source code. diff --git a/src/source.rs b/src/source.rs index 82b545505..eaf94001b 100644 --- a/src/source.rs +++ b/src/source.rs @@ -20,24 +20,24 @@ use codespan_reporting::files::{self, Files}; /// A unique identifier for a loaded source file. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct SourceId(u32); +pub struct SourceId(u16); impl SourceId { /// Create a new source id for a file that is not part of a store. pub const fn detached() -> Self { - Self(u32::MAX) + Self(u16::MAX) } /// Create a source id from the raw underlying value. /// /// This should only be called with values returned by /// [`into_raw`](Self::into_raw). - pub const fn from_raw(v: u32) -> Self { + pub const fn from_raw(v: u16) -> Self { Self(v) } /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u32 { + pub const fn into_raw(self) -> u16 { self.0 } } @@ -108,7 +108,7 @@ impl SourceStore { } // No existing file yet. - let id = SourceId(self.sources.len() as u32); + let id = SourceId(self.sources.len() as u16); self.sources.push(SourceFile::new(id, path, src)); // Register in file map if the path was known to the loader. @@ -140,6 +140,13 @@ impl SourceStore { ) -> Range { self.sources[id.0 as usize].edit(replace, with) } + + /// Map a span that points into this source store to a byte range. + /// + /// Panics if the span does not point into this source store. + pub fn range(&self, span: Span) -> Range { + self.get(span.source()).range(span) + } } /// A single source file. @@ -160,10 +167,14 @@ impl SourceFile { pub fn new(id: SourceId, path: &Path, src: String) -> Self { let mut lines = vec![Line { byte_idx: 0, utf16_idx: 0 }]; lines.extend(Line::iter(0, 0, &src)); + + let mut root = parse(&src); + root.number(id, 1); + Self { id, path: path.normalize(), - root: parse(&src), + root, src, lines, rev: 0, @@ -178,8 +189,8 @@ impl SourceFile { /// Create a source file with the same synthetic span for all nodes. pub fn synthesized(src: impl Into, span: Span) -> Self { let mut file = Self::detached(src); - file.root.synthesize(Arc::new(span)); - file.id = span.source; + file.root.synthesize(span); + file.id = span.source(); file } @@ -232,6 +243,7 @@ impl SourceFile { self.lines = vec![Line { byte_idx: 0, utf16_idx: 0 }]; self.lines.extend(Line::iter(0, 0, &self.src)); self.root = parse(&self.src); + self.root.number(self.id(), 1); self.rev = self.rev.wrapping_add(1); } @@ -265,7 +277,9 @@ impl SourceFile { )); // Incrementally reparse the replaced range. - reparse(&mut self.root, &self.src, replace, with.len()) + let range = reparse(&mut self.root, &self.src, replace, with.len()); + self.root.number(self.id(), 1); + range } /// Get the length of the file in bytes. @@ -284,6 +298,15 @@ impl SourceFile { self.lines.len() } + /// Map a span that points into this source file to a byte range. + /// + /// Panics if the span does not point into this source file. + pub fn range(&self, span: Span) -> Range { + self.root + .range(span, 0) + .expect("span does not point into this source file") + } + /// Return the index of the UTF-16 code unit at the byte index. pub fn byte_to_utf16(&self, byte_idx: usize) -> Option { let line_idx = self.byte_to_line(byte_idx)?; diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 1b84fdba0..630a451df 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -10,24 +10,38 @@ use crate::parse::TokenMode; /// Provide highlighting categories for the descendants of a node that fall into /// a range. -pub fn highlight_node(node: &SyntaxNode, range: Range, f: &mut F) +pub fn highlight_node(root: &SyntaxNode, range: Range, mut f: F) where F: FnMut(Range, Category), +{ + highlight_node_impl(0, root, range, &mut f) +} + +/// Provide highlighting categories for the descendants of a node that fall into +/// a range. +pub fn highlight_node_impl( + mut offset: usize, + node: &SyntaxNode, + range: Range, + f: &mut F, +) where + F: FnMut(Range, Category), { for (i, child) in node.children().enumerate() { - let span = child.span(); + let span = offset .. offset + child.len(); if range.start <= span.end && range.end >= span.start { if let Some(category) = Category::determine(child, node, i) { - f(span.to_range(), category); + f(span, category); } - highlight_node(child, range.clone(), f); + highlight_node_impl(offset, child, range.clone(), f); } + offset += child.len(); } } /// Highlight source text in a theme by calling `f` with each consecutive piece /// and its style. -pub fn highlight_themed(text: &str, mode: TokenMode, theme: &Theme, f: &mut F) +pub fn highlight_themed(text: &str, mode: TokenMode, theme: &Theme, mut f: F) where F: FnMut(&str, Style), { @@ -43,12 +57,13 @@ where }; let highlighter = Highlighter::new(&theme); - highlight_themed_impl(text, &root, vec![], &highlighter, f); + highlight_themed_impl(text, 0, &root, vec![], &highlighter, &mut f); } /// Recursive implementation for returning syntect styles. fn highlight_themed_impl( text: &str, + mut offset: usize, node: &SyntaxNode, scopes: Vec, highlighter: &Highlighter, @@ -57,7 +72,7 @@ fn highlight_themed_impl( F: FnMut(&str, Style), { if node.children().len() == 0 { - let piece = &text[node.span().to_range()]; + let piece = &text[offset .. offset + node.len()]; let style = highlighter.style_for_stack(&scopes); f(piece, style); return; @@ -68,7 +83,8 @@ fn highlight_themed_impl( if let Some(category) = Category::determine(child, node, i) { scopes.push(Scope::new(category.tm_scope()).unwrap()) } - highlight_themed_impl(text, child, scopes, highlighter, f); + highlight_themed_impl(text, offset, child, scopes, highlighter, f); + offset += child.len(); } } @@ -92,7 +108,7 @@ pub fn highlight_pre(text: &str, mode: TokenMode, theme: &Theme) -> String { let mut buf = String::new(); buf.push_str("
\n");
 
-    highlight_themed(text, mode, theme, &mut |piece, style| {
+    highlight_themed(text, mode, theme, |piece, style| {
         let styled = style != Style::default();
         if styled {
             buf.push_str(" &NodeData {
         match self {
-            SyntaxNode::Inner(n) => &n.data,
-            SyntaxNode::Leaf(t) => t,
+            SyntaxNode::Inner(inner) => &inner.data,
+            SyntaxNode::Leaf(leaf) => leaf,
         }
     }
 
@@ -44,15 +45,31 @@ impl SyntaxNode {
         self.data().len()
     }
 
+    /// The number of descendants, including the node itself.
+    pub fn count(&self) -> usize {
+        match self {
+            SyntaxNode::Inner(inner) => inner.count(),
+            SyntaxNode::Leaf(_) => 1,
+        }
+    }
+
     /// The span of the node.
     pub fn span(&self) -> Span {
-        todo!()
+        self.data().span()
+    }
+
+    /// If the span points into this node, convert it to a byte range.
+    pub fn range(&self, span: Span, offset: usize) -> Option> {
+        match self {
+            SyntaxNode::Inner(inner) => inner.range(span, offset),
+            SyntaxNode::Leaf(leaf) => leaf.range(span, offset),
+        }
     }
 
     /// The node's children.
     pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> {
         match self {
-            SyntaxNode::Inner(n) => n.children(),
+            SyntaxNode::Inner(inner) => inner.children(),
             SyntaxNode::Leaf(_) => [].iter(),
         }
     }
@@ -62,7 +79,7 @@ impl SyntaxNode {
     /// This method is slow and only intended for testing.
     pub fn leafs(&self) -> Vec {
         if match self {
-            SyntaxNode::Inner(n) => n.children().len() == 0,
+            SyntaxNode::Inner(inner) => inner.children().len() == 0,
             SyntaxNode::Leaf(_) => true,
         } {
             vec![self.clone()]
@@ -86,7 +103,9 @@ impl SyntaxNode {
         }
 
         match self.kind() {
-            NodeKind::Error(..) => todo!(),
+            &NodeKind::Error(pos, ref message) => {
+                vec![Error { pos, ..Error::new(self.span(), message) }]
+            }
             _ => self
                 .children()
                 .filter(|node| node.erroneous())
@@ -116,20 +135,28 @@ impl SyntaxNode {
     /// Change the type of the node.
     pub fn convert(&mut self, kind: NodeKind) {
         match self {
-            Self::Inner(node) => {
-                let node = Arc::make_mut(node);
+            Self::Inner(inner) => {
+                let node = Arc::make_mut(inner);
                 node.erroneous |= kind.is_error();
                 node.data.kind = kind;
             }
-            Self::Leaf(data) => data.kind = kind,
+            Self::Leaf(leaf) => leaf.kind = kind,
         }
     }
 
-    /// Set a synthetic span for the node and all its children.
-    pub fn synthesize(&mut self, span: Arc) {
+    /// Assign spans to each node.
+    pub fn number(&mut self, id: SourceId, from: u64) {
         match self {
-            SyntaxNode::Inner(n) => Arc::make_mut(n).synthesize(span),
-            SyntaxNode::Leaf(t) => t.synthesize(span),
+            SyntaxNode::Inner(inner) => Arc::make_mut(inner).number(id, from),
+            SyntaxNode::Leaf(leaf) => leaf.number(id, from),
+        }
+    }
+
+    /// Set a synthetic node id for the node and all its descendants.
+    pub fn synthesize(&mut self, span: Span) {
+        match self {
+            SyntaxNode::Inner(inner) => Arc::make_mut(inner).synthesize(span),
+            SyntaxNode::Leaf(leaf) => leaf.synthesize(span),
         }
     }
 }
@@ -154,10 +181,12 @@ impl Debug for SyntaxNode {
 pub struct InnerNode {
     /// Node metadata.
     data: NodeData,
-    /// This node's children, losslessly make up this node.
-    children: Vec,
+    /// The number of nodes in the whole subtree, including this node.
+    count: usize,
     /// Whether this node or any of its children are erroneous.
     erroneous: bool,
+    /// This node's children, losslessly make up this node.
+    children: Vec,
 }
 
 impl InnerNode {
@@ -168,17 +197,21 @@ impl InnerNode {
 
     /// Creates a new node with the given kind and children.
     pub fn with_children(kind: NodeKind, children: Vec) -> Self {
+        let mut len = 0;
+        let mut count = 1;
         let mut erroneous = kind.is_error();
-        let len = children
-            .iter()
-            .inspect(|c| erroneous |= c.erroneous())
-            .map(SyntaxNode::len)
-            .sum();
+
+        for child in &children {
+            len += child.len();
+            count += child.count();
+            erroneous |= child.erroneous();
+        }
 
         Self {
             data: NodeData::new(kind, len),
-            children,
+            count,
             erroneous,
+            children,
         }
     }
 
@@ -197,16 +230,54 @@ impl InnerNode {
         self.data().len()
     }
 
+    /// The node's span.
+    pub fn span(&self) -> Span {
+        self.data().span()
+    }
+
+    /// The number of descendants, including the node itself.
+    pub fn count(&self) -> usize {
+        self.count
+    }
+
+    /// If the span points into this node, convert it to a byte range.
+    pub fn range(&self, span: Span, mut offset: usize) -> Option> {
+        if let Some(range) = self.data.range(span, offset) {
+            return Some(range);
+        }
+
+        for child in &self.children {
+            if let Some(range) = child.range(span, offset) {
+                return Some(range);
+            }
+
+            offset += child.len();
+        }
+
+        None
+    }
+
     /// The node's children.
     pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> {
         self.children.iter()
     }
 
-    /// Set a synthetic span for the node and all its children.
-    pub fn synthesize(&mut self, span: Arc) {
-        self.data.synthesize(span.clone());
+    /// Assign spans to each node.
+    pub fn number(&mut self, id: SourceId, mut from: u64) {
+        self.data.number(id, from);
+        from += 1;
+
         for child in &mut self.children {
-            child.synthesize(span.clone());
+            child.number(id, from);
+            from += child.count() as u64;
+        }
+    }
+
+    /// Set a synthetic node id for the node and all its descendants.
+    pub fn synthesize(&mut self, span: Span) {
+        self.data.synthesize(span);
+        for child in &mut self.children {
+            child.synthesize(span);
         }
     }
 
@@ -222,24 +293,41 @@ impl InnerNode {
         replacement: Vec,
     ) {
         let superseded = &self.children[range.clone()];
-        let superseded_len: usize = superseded.iter().map(SyntaxNode::len).sum();
-        let replacement_len: usize = replacement.iter().map(SyntaxNode::len).sum();
 
-        // If we're erroneous, but not due to the superseded range, then we will
-        // still be erroneous after the replacement.
-        let still_erroneous =
-            self.erroneous && !superseded.iter().any(SyntaxNode::erroneous);
+        // Compute the new byte length.
+        self.data.len = self.data.len
+            + replacement.iter().map(SyntaxNode::len).sum::()
+            - superseded.iter().map(SyntaxNode::len).sum::();
 
+        // Compute the new descendant count.
+        self.count = self.count
+            + replacement.iter().map(SyntaxNode::count).sum::()
+            - superseded.iter().map(SyntaxNode::count).sum::();
+
+        // Determine whether we're still erroneous after the replacement. That's
+        // the case if
+        // - any of the new nodes is erroneous,
+        // - or if we were erroneous before due to a non-superseded node.
+        self.erroneous = replacement.iter().any(SyntaxNode::erroneous)
+            || (self.erroneous
+                && (self.children[.. range.start].iter().any(SyntaxNode::erroneous)
+                    || self.children[range.end ..].iter().any(SyntaxNode::erroneous)));
+
+        // Perform the replacement.
         self.children.splice(range, replacement);
-        self.data.len = self.data.len + replacement_len - superseded_len;
-        self.erroneous =
-            still_erroneous || self.children.iter().any(SyntaxNode::erroneous);
     }
 
     /// Update the length of this node given the old and new length of
     /// replaced children.
-    pub(crate) fn update_parent(&mut self, new_len: usize, old_len: usize) {
-        self.data.len = self.data.len() + new_len - old_len;
+    pub(crate) fn update_parent(
+        &mut self,
+        prev_len: usize,
+        new_len: usize,
+        prev_count: usize,
+        new_count: usize,
+    ) {
+        self.data.len = self.data.len + new_len - prev_len;
+        self.count = self.count + new_count - prev_count;
         self.erroneous = self.children.iter().any(SyntaxNode::erroneous);
     }
 }
@@ -268,34 +356,51 @@ impl Debug for InnerNode {
 }
 
 /// Data shared between inner and leaf nodes.
-#[derive(Clone, PartialEq, Hash)]
+#[derive(Clone, Hash)]
 pub struct NodeData {
     /// What kind of node this is (each kind would have its own struct in a
     /// strongly typed AST).
     kind: NodeKind,
     /// The byte length of the node in the source.
     len: usize,
+    /// The node's span.
+    span: Span,
 }
 
 impl NodeData {
     /// Create new node metadata.
     pub fn new(kind: NodeKind, len: usize) -> Self {
-        Self { len, kind }
+        Self { len, kind, span: Span::detached() }
     }
 
-    /// The type of the node.
+    /// The node's type.
     pub fn kind(&self) -> &NodeKind {
         &self.kind
     }
 
-    /// The length of the node.
+    /// The node's length.
     pub fn len(&self) -> usize {
         self.len
     }
 
+    /// The node's span.
+    pub fn span(&self) -> Span {
+        self.span
+    }
+
+    /// If the span points into this node, convert it to a byte range.
+    pub fn range(&self, span: Span, offset: usize) -> Option> {
+        (self.span == span).then(|| offset .. offset + self.len())
+    }
+
+    /// Assign spans to each node.
+    pub fn number(&mut self, id: SourceId, from: u64) {
+        self.span = Span::new(id, from);
+    }
+
     /// Set a synthetic span for the node.
-    pub fn synthesize(&mut self, _: Arc) {
-        todo!()
+    pub fn synthesize(&mut self, span: Span) {
+        self.span = span;
     }
 }
 
@@ -311,6 +416,12 @@ impl Debug for NodeData {
     }
 }
 
+impl PartialEq for NodeData {
+    fn eq(&self, other: &Self) -> bool {
+        self.kind == other.kind && self.len == other.len
+    }
+}
+
 /// All syntactical building blocks that can be part of a Typst document.
 ///
 /// Can be emitted as a token by the tokenizer or as part of a syntax node by
@@ -547,17 +658,6 @@ pub enum NodeKind {
     Unknown(EcoString),
 }
 
-/// Where in a node an error should be annotated.
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-pub enum ErrorPos {
-    /// At the start of the node.
-    Start,
-    /// Over the full width of the node.
-    Full,
-    /// At the end of the node.
-    End,
-}
-
 impl NodeKind {
     /// Whether this is some kind of brace.
     pub fn is_brace(&self) -> bool {
diff --git a/src/syntax/span.rs b/src/syntax/span.rs
index d1e29dd36..3f6e68240 100644
--- a/src/syntax/span.rs
+++ b/src/syntax/span.rs
@@ -1,8 +1,7 @@
-use std::cmp::Ordering;
 use std::fmt::{self, Debug, Formatter};
-use std::ops::Range;
+use std::num::NonZeroU64;
 
-use crate::source::SourceId;
+use crate::syntax::SourceId;
 
 /// A value with the span it corresponds to in the source code.
 #[derive(Copy, Clone, Eq, PartialEq, Hash)]
@@ -35,122 +34,52 @@ impl Spanned {
 
 impl Debug for Spanned {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        self.v.fmt(f)?;
-        if f.alternate() {
-            f.write_str(" <")?;
-            self.span.fmt(f)?;
-            f.write_str(">")?;
-        }
-        Ok(())
+        self.v.fmt(f)
     }
 }
 
-/// Bounds of a slice of source code.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Span {
-    /// The id of the source file.
-    pub source: SourceId,
-    /// The inclusive start position.
-    pub start: usize,
-    /// The inclusive end position.
-    pub end: usize,
-}
+/// A unique identifier for a syntax node.
+///
+/// This is used throughout the compiler to track which source section an error
+/// or element stems from. Can be mapped back to a source id + byte range for
+/// user facing display.
+///
+/// Node ids are ordered in the tree to enable quickly finding the node with
+/// some id:
+/// - The id of a parent is always smaller than the ids of any of its children.
+/// - The id of a node is always greater than any id in the subtrees of any left
+///   sibling and smaller than any id the subtrees of any right sibling.
+///
+/// Node ids stay mostly stable, even for nodes behind an insertion. This is not
+/// true for simple spans/ranges as they shift. Node ids can be used as inputs
+/// to memoized functions without hurting cache performance when text is
+/// inserted somewhere in the document other than the end.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Span(NonZeroU64);
 
 impl Span {
-    /// Create a new span from start and end positions.
-    pub fn new(source: SourceId, start: usize, end: usize) -> Self {
-        Self { source, start, end }
+    /// Create a new span from a source id and a unique number.
+    pub const fn new(id: SourceId, number: u64) -> Self {
+        assert!(number > 0 && number < (1 << 48));
+        let bits = ((id.into_raw() as u64) << 48) | number;
+        Self(nonzero(bits))
     }
 
-    /// Create a span including just a single position.
-    pub fn at(source: SourceId, pos: usize) -> Self {
-        Self::new(source, pos, pos)
+    /// A node that does not belong to any source file.
+    pub const fn detached() -> Self {
+        Self(nonzero(1))
     }
 
-    /// Create a span without real location information, usually for testing.
-    pub fn detached() -> Self {
-        Self {
-            source: SourceId::from_raw(0),
-            start: 0,
-            end: 0,
-        }
-    }
-
-    /// Create a span with a different start position.
-    pub fn with_start(self, start: usize) -> Self {
-        Self { start, ..self }
-    }
-
-    /// Create a span with a different end position.
-    pub fn with_end(self, end: usize) -> Self {
-        Self { end, ..self }
-    }
-
-    /// Whether the span is a single point.
-    pub fn is_empty(self) -> bool {
-        self.start == self.end
-    }
-
-    /// The byte length of the spanned region.
-    pub fn len(self) -> usize {
-        self.end - self.start
-    }
-
-    /// A new span at the position of this span's start.
-    pub fn at_start(&self) -> Span {
-        Self::at(self.source, self.start)
-    }
-
-    /// A new span at the position of this span's end.
-    pub fn at_end(&self) -> Span {
-        Self::at(self.source, self.end)
-    }
-
-    /// Create a new span with the earlier start and later end position.
-    ///
-    /// This panics if the spans come from different files.
-    pub fn join(self, other: Self) -> Self {
-        debug_assert_eq!(self.source, other.source);
-        Self {
-            source: self.source,
-            start: self.start.min(other.start),
-            end: self.end.max(other.end),
-        }
-    }
-
-    /// Expand a span by merging it with another span.
-    pub fn expand(&mut self, other: Self) {
-        *self = self.join(other)
-    }
-
-    /// Test whether a position is within the span.
-    pub fn contains(&self, pos: usize) -> bool {
-        self.start <= pos && self.end >= pos
-    }
-
-    /// Test whether one span complete contains the other span.
-    pub fn surrounds(self, other: Self) -> bool {
-        self.source == other.source && self.start <= other.start && self.end >= other.end
-    }
-
-    /// Convert to a `Range` for indexing.
-    pub fn to_range(self) -> Range {
-        self.start .. self.end
+    /// The id of the source file the span points into.
+    pub const fn source(self) -> SourceId {
+        SourceId::from_raw((self.0.get() >> 48) as u16)
     }
 }
 
-impl Debug for Span {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        write!(f, "{:?}-{:?}", self.start, self.end)
-    }
-}
-
-impl PartialOrd for Span {
-    fn partial_cmp(&self, other: &Self) -> Option {
-        if self.source == other.source {
-            Some(self.start.cmp(&other.start).then(self.end.cmp(&other.end)))
-        } else {
-            None
-        }
+/// Convert to a non zero u64.
+const fn nonzero(v: u64) -> NonZeroU64 {
+    match NonZeroU64::new(v) {
+        Some(v) => v,
+        None => unreachable!(),
     }
 }
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 2eb847ce0..56cde751c 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -9,7 +9,7 @@ use tiny_skia as sk;
 use unscanny::Scanner;
 use walkdir::WalkDir;
 
-use typst::diag::Error;
+use typst::diag::ErrorPos;
 use typst::eval::{Smart, Value};
 use typst::frame::{Element, Frame};
 use typst::geom::{Length, RgbaColor, Sides};
@@ -18,7 +18,6 @@ use typst::library::text::{TextNode, TextSize};
 use typst::loading::FsLoader;
 use typst::model::StyleMap;
 use typst::source::SourceFile;
-use typst::syntax::Span;
 use typst::{bail, Config, Context};
 
 const TYP_DIR: &str = "./typ";
@@ -301,7 +300,7 @@ fn test_part(
 
     ok &= test_reparse(ctx.sources.get(id).src(), i, rng);
 
-    let (mut frames, mut errors) = match typst::typeset(ctx, id) {
+    let (mut frames, errors) = match typst::typeset(ctx, id) {
         Ok(frames) => (frames, vec![]),
         Err(errors) => (vec![], *errors),
     };
@@ -311,15 +310,24 @@ fn test_part(
         frames.clear();
     }
 
-    // TODO: Also handle errors from other files.
-    errors.retain(|error| error.span.source == id);
-    for error in &mut errors {
-        error.trace.clear();
-    }
+    // Map errors to range and message format, discard traces and errors from
+    // other files.
+    let mut errors: Vec<_> = errors
+        .into_iter()
+        .filter(|error| error.span.source() == id)
+        .map(|error| {
+            let mut range = ctx.sources.range(error.span);
+            match error.pos {
+                ErrorPos::Start => range.end = range.start,
+                ErrorPos::Full => {}
+                ErrorPos::End => range.start = range.end,
+            }
+            (range, error.message)
+        })
+        .collect();
 
-    // The comparison never fails since all spans are from the same source file.
-    ref_errors.sort_by(|a, b| a.span.partial_cmp(&b.span).unwrap());
-    errors.sort_by(|a, b| a.span.partial_cmp(&b.span).unwrap());
+    errors.sort_by_key(|error| error.0.start);
+    ref_errors.sort_by_key(|error| error.0.start);
 
     if errors != ref_errors {
         println!("  Subtest {i} does not match expected errors. ❌");
@@ -327,7 +335,7 @@ fn test_part(
 
         let source = ctx.sources.get(id);
         for error in errors.iter() {
-            if error.span.source == id && !ref_errors.contains(error) {
+            if !ref_errors.contains(error) {
                 print!("    Not annotated | ");
                 print_error(&source, line, error);
             }
@@ -344,7 +352,7 @@ fn test_part(
     (ok, compare_ref, frames)
 }
 
-fn parse_metadata(source: &SourceFile) -> (Option, Vec) {
+fn parse_metadata(source: &SourceFile) -> (Option, Vec<(Range, String)>) {
     let mut compare_ref = None;
     let mut errors = vec![];
 
@@ -382,23 +390,24 @@ fn parse_metadata(source: &SourceFile) -> (Option, Vec) {
         let mut s = Scanner::new(rest);
         let start = pos(&mut s);
         let end = if s.eat_if('-') { pos(&mut s) } else { start };
-        let span = Span::new(source.id(), start, end);
+        let range = start .. end;
 
-        errors.push(Error::new(span, s.after().trim()));
+        errors.push((range, s.after().trim().to_string()));
     }
 
     (compare_ref, errors)
 }
 
-fn print_error(source: &SourceFile, line: usize, error: &Error) {
-    let start_line = 1 + line + source.byte_to_line(error.span.start).unwrap();
-    let start_col = 1 + source.byte_to_column(error.span.start).unwrap();
-    let end_line = 1 + line + source.byte_to_line(error.span.end).unwrap();
-    let end_col = 1 + source.byte_to_column(error.span.end).unwrap();
-    println!(
-        "Error: {start_line}:{start_col}-{end_line}:{end_col}: {}",
-        error.message,
-    );
+fn print_error(
+    source: &SourceFile,
+    line: usize,
+    (range, message): &(Range, String),
+) {
+    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 end_col = 1 + source.byte_to_column(range.end).unwrap();
+    println!("Error: {start_line}:{start_col}-{end_line}:{end_col}: {message}");
 }
 
 /// Pseudorandomly edit the source file and test whether a reparse produces the
@@ -487,10 +496,11 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
         ok &= apply(start .. end, supplement);
     }
 
-    let leafs = typst::parse::parse(src).leafs();
-    let leaf_start = leafs[pick(0 .. leafs.len())].span().start;
+    let source = SourceFile::detached(src);
+    let leafs = source.root().leafs();
+    let start = source.range(leafs[pick(0 .. leafs.len())].span()).start;
     let supplement = supplements[pick(0 .. supplements.len())];
-    ok &= apply(leaf_start .. leaf_start, supplement);
+    ok &= apply(start .. start, supplement);
 
     ok
 }