diff --git a/Cargo.lock b/Cargo.lock index e8a65066a..e1eabd019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,15 +374,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "getrandom" version = "0.2.7" @@ -1126,7 +1117,6 @@ dependencies = [ "dirs", "elsa", "flate2", - "fxhash", "hypher", "iai", "image", diff --git a/Cargo.toml b/Cargo.toml index 1c96229bf..402419deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,25 +11,23 @@ typst-macros = { path = "./macros" } # Utilities bitflags = "1" bytemuck = "1" -fxhash = "0.2" +comemo = "0.1" once_cell = "1" +regex = "1" serde = { version = "1", features = ["derive"] } +siphasher = "0.3" typed-arena = "2" unscanny = "0.1" -regex = "1" - -# Incremental compilation -comemo = "0.1" # Text and font handling hypher = "0.1" kurbo = "0.8" -ttf-parser = "0.17" rustybuzz = "0.5" +ttf-parser = "0.17" unicode-bidi = "0.3.5" +unicode-script = "0.5" unicode-segmentation = "1" unicode-xid = "0.2" -unicode-script = "0.5" xi-unicode = "0.3" # Raster and vector graphics handling @@ -37,12 +35,12 @@ image = { version = "0.24", default-features = false, features = ["png", "jpeg", usvg = { version = "0.22", default-features = false } # External implementation of user-facing features -syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } -rex = { git = "https://github.com/laurmaedje/ReX" } -unicode-math = { git = "https://github.com/s3bk/unicode-math/" } -lipsum = { git = "https://github.com/reknih/lipsum" } csv = "1" +lipsum = { git = "https://github.com/reknih/lipsum" } +rex = { git = "https://github.com/laurmaedje/ReX" } serde_json = "1" +syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } +unicode-math = { git = "https://github.com/s3bk/unicode-math/" } # PDF export miniz_oxide = "0.5" @@ -50,52 +48,50 @@ pdf-writer = "0.6" subsetter = "0.1" svg2pdf = "0.4" -# Raster export / rendering -tiny-skia = "0.6.2" +# Rendering +flate2 = "1" pixglyph = { git = "https://github.com/typst/pixglyph" } resvg = { version = "0.22", default-features = false } roxmltree = "0.14" -flate2 = "1" +tiny-skia = "0.6.2" -# Command line interface / tests -pico-args = { version = "0.4", optional = true } +# Command line interface +chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } codespan-reporting = { version = "0.11", optional = true } +dirs = { version = "4", optional = true } +elsa = { version = "1.7", optional = true } +memmap2 = { version = "0.5", optional = true } +notify = { version = "5", optional = true } +pico-args = { version = "0.4", optional = true } same-file = { version = "1", optional = true } walkdir = { version = "2", optional = true } -elsa = { version = "1.7", optional = true } -dirs = { version = "4", optional = true } -memmap2 = { version = "0.5", optional = true } -siphasher = { version = "0.3", optional = true } -notify = { version = "5", optional = true } -chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } [dev-dependencies] iai = { git = "https://github.com/reknih/iai" } walkdir = "2" +elsa = "1.7" + +[workspace] +members = ["macros"] [features] -default = ["tests"] -tests = ["same-file", "walkdir", "elsa", "siphasher"] cli = [ - "pico-args", + "chrono", "codespan-reporting", "dirs", + "elsa", "memmap2", + "notify", + "pico-args", "same-file", "walkdir", - "elsa", - "siphasher", - "notify", - "chrono", ] [profile.dev] -# Faster compilation -debug = 0 +debug = 0 # Faster compilation [profile.dev.package."*"] -# Faster test execution -opt-level = 2 +opt-level = 2 # Faster test execution [[bin]] name = "typst" @@ -103,7 +99,6 @@ required-features = ["cli"] [[test]] name = "typeset" -required-features = ["tests"] harness = false [[bench]] diff --git a/benches/oneshot.rs b/benches/oneshot.rs index d47275129..1d82f44d9 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -6,8 +6,7 @@ use unscanny::Scanner; use typst::diag::{FileError, FileResult}; use typst::font::{Font, FontBook}; -use typst::parse::{TokenMode, Tokens}; -use typst::source::{Source, SourceId}; +use typst::syntax::{Source, SourceId, TokenMode, Tokens}; use typst::util::Buffer; use typst::{Config, World}; @@ -55,7 +54,7 @@ fn bench_tokenize(iai: &mut Iai) { } fn bench_parse(iai: &mut Iai) { - iai.run(|| typst::parse::parse(TEXT)); + iai.run(|| typst::syntax::parse(TEXT)); } fn bench_edit(iai: &mut Iai) { @@ -77,15 +76,15 @@ fn bench_highlight(iai: &mut Iai) { fn bench_eval(iai: &mut Iai) { let world = BenchWorld::new(); let id = world.source.id(); - let route = typst::eval::Route::default(); - iai.run(|| typst::eval::eval(world.track(), route.track(), id).unwrap()); + let route = typst::model::Route::default(); + iai.run(|| typst::model::eval(world.track(), route.track(), id).unwrap()); } fn bench_layout(iai: &mut Iai) { let world = BenchWorld::new(); let id = world.source.id(); - let route = typst::eval::Route::default(); - let module = typst::eval::eval(world.track(), route.track(), id).unwrap(); + let route = typst::model::Route::default(); + let module = typst::model::eval(world.track(), route.track(), id).unwrap(); iai.run(|| typst::model::layout(world.track(), &module.content)); } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 052e7fcfc..62e27a092 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -69,12 +69,12 @@ fn expand(stream: TokenStream2, mut impl_block: syn::ItemImpl) -> Result eval::Node for #self_ty { + impl<#params> model::Node for #self_ty { const SHOWABLE: bool = #showable; #construct #set diff --git a/src/frame.rs b/src/frame.rs index 287bbf908..c7827a9ce 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,13 +4,13 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::num::NonZeroUsize; use std::sync::Arc; -use crate::eval::{Dict, Value}; use crate::font::Font; use crate::geom::{ Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform, }; use crate::image::Image; use crate::library::text::Lang; +use crate::model::{Dict, Value}; use crate::util::EcoString; /// A finished layout with elements at fixed positions. diff --git a/src/lib.rs b/src/lib.rs index e288d556a..25f59aae1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,12 +17,12 @@ //! - **Exporting:** The finished layout can be exported into a supported //! format. Currently, the only supported output format is [PDF]. //! -//! [tokens]: parse::Tokens -//! [parsed]: parse::parse +//! [tokens]: syntax::Tokens +//! [parsed]: syntax::parse //! [syntax tree]: syntax::SyntaxNode //! [AST]: syntax::ast -//! [evaluate]: eval::eval -//! [module]: eval::Module +//! [evaluate]: model::eval +//! [module]: model::Module //! [content]: model::Content //! [layouted]: model::layout //! [PDF]: export::pdf @@ -38,15 +38,12 @@ pub mod geom; #[macro_use] pub mod diag; #[macro_use] -pub mod eval; +pub mod model; pub mod export; pub mod font; pub mod frame; pub mod image; pub mod library; -pub mod model; -pub mod parse; -pub mod source; pub mod syntax; use std::path::{Path, PathBuf}; @@ -54,11 +51,11 @@ use std::path::{Path, PathBuf}; use comemo::{Prehashed, Track}; use crate::diag::{FileResult, SourceResult}; -use crate::eval::{Route, Scope}; use crate::font::{Font, FontBook}; use crate::frame::Frame; use crate::model::StyleMap; -use crate::source::{Source, SourceId}; +use crate::model::{Route, Scope}; +use crate::syntax::{Source, SourceId}; use crate::util::Buffer; /// Typeset a source file into a collection of layouted frames. @@ -71,7 +68,7 @@ pub fn typeset( main: SourceId, ) -> SourceResult> { let route = Route::default(); - let module = eval::eval(world.track(), route.track(), main)?; + let module = model::eval(world.track(), route.track(), main)?; model::layout(world.track(), &module.content) } diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 03bef51e1..7a4284f16 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -12,12 +12,12 @@ pub use typst_macros::node; pub use crate::diag::{ with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult, }; -pub use crate::eval::{ +pub use crate::frame::*; +pub use crate::geom::*; +pub use crate::model::{ Arg, Args, Array, Cast, Dict, Dynamic, Func, Node, RawAlign, RawLength, RawStroke, Scope, Smart, Str, Value, Vm, }; -pub use crate::frame::*; -pub use crate::geom::*; pub use crate::model::{ Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Selector, Show, ShowNode, StyleChain, StyleMap, StyleVec, diff --git a/src/library/text/lang.rs b/src/library/text/lang.rs index b75b3cd82..f2193f050 100644 --- a/src/library/text/lang.rs +++ b/src/library/text/lang.rs @@ -1,5 +1,5 @@ -use crate::eval::Value; use crate::geom::Dir; +use crate::model::Value; /// A code for a natural language. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/src/library/text/quotes.rs b/src/library/text/quotes.rs index 98402ca4f..0a22646a1 100644 --- a/src/library/text/quotes.rs +++ b/src/library/text/quotes.rs @@ -1,5 +1,5 @@ use super::{Lang, Region}; -use crate::parse::is_newline; +use crate::syntax::is_newline; /// State machine for smart quote subtitution. #[derive(Debug, Clone)] diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index 8b0874f82..351600736 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -72,8 +72,8 @@ impl Show for RawNode { let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { let root = match lang.as_deref() { - Some("typc") => crate::parse::parse_code(&self.text), - _ => crate::parse::parse(&self.text), + Some("typc") => crate::syntax::parse_code(&self.text), + _ => crate::syntax::parse(&self.text), }; let mut seq = vec![]; diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index d9b19d64f..2d637d298 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -12,9 +12,9 @@ pub use string::*; use comemo::Track; -use crate::eval::{Eval, Route, Scopes, Vm}; use crate::library::prelude::*; -use crate::source::Source; +use crate::model::{Eval, Route, Scopes, Vm}; +use crate::syntax::Source; /// The name of a value's type. pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult { diff --git a/src/library/utility/string.rs b/src/library/utility/string.rs index 91a990a97..66f127d10 100644 --- a/src/library/utility/string.rs +++ b/src/library/utility/string.rs @@ -1,5 +1,5 @@ -use crate::eval::Regex; use crate::library::prelude::*; +use crate::model::Regex; /// The string representation of a value. pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult { diff --git a/src/main.rs b/src/main.rs index ef4410913..e32bb8c60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use walkdir::WalkDir; use typst::diag::{FileError, FileResult, SourceError, StrResult}; use typst::font::{Font, FontBook, FontInfo, FontVariant}; -use typst::source::{Source, SourceId}; +use typst::syntax::{Source, SourceId}; use typst::util::{Buffer, PathExt}; use typst::{Config, World}; diff --git a/src/eval/args.rs b/src/model/args.rs similarity index 100% rename from src/eval/args.rs rename to src/model/args.rs diff --git a/src/eval/array.rs b/src/model/array.rs similarity index 98% rename from src/eval/array.rs rename to src/model/array.rs index b77ce93c5..196f02ecf 100644 --- a/src/eval/array.rs +++ b/src/model/array.rs @@ -12,11 +12,11 @@ use crate::util::ArcExt; #[allow(unused_macros)] macro_rules! array { ($value:expr; $count:expr) => { - $crate::eval::Array::from_vec(vec![$value.into(); $count]) + $crate::model::Array::from_vec(vec![$value.into(); $count]) }; ($($value:expr),* $(,)?) => { - $crate::eval::Array::from_vec(vec![$($value.into()),*]) + $crate::model::Array::from_vec(vec![$($value.into()),*]) }; } diff --git a/src/eval/capture.rs b/src/model/capture.rs similarity index 99% rename from src/eval/capture.rs rename to src/model/capture.rs index 289d31e18..c4c107b2e 100644 --- a/src/eval/capture.rs +++ b/src/model/capture.rs @@ -133,7 +133,7 @@ impl<'a> CapturesVisitor<'a> { #[cfg(test)] mod tests { use super::*; - use crate::parse::parse; + use crate::syntax::parse; #[track_caller] fn test(text: &str, result: &[&str]) { diff --git a/src/eval/cast.rs b/src/model/cast.rs similarity index 91% rename from src/eval/cast.rs rename to src/model/cast.rs index 99c348102..00a3fe456 100644 --- a/src/eval/cast.rs +++ b/src/model/cast.rs @@ -1,9 +1,8 @@ use std::num::NonZeroUsize; -use super::{Regex, Value}; +use super::{Content, Layout, LayoutNode, Pattern, Regex, Value}; use crate::diag::{with_alternative, StrResult}; use crate::geom::{Corners, Dir, Paint, Sides}; -use crate::model::{Content, Layout, LayoutNode, Pattern}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -19,20 +18,20 @@ pub trait Cast: Sized { /// Implement traits for dynamic types. macro_rules! dynamic { ($type:ty: $name:literal, $($tts:tt)*) => { - impl $crate::eval::Type for $type { + impl $crate::model::Type for $type { const TYPE_NAME: &'static str = $name; } castable! { $type, - Expected: ::TYPE_NAME, + Expected: ::TYPE_NAME, $($tts)* @this: Self => this.clone(), } - impl From<$type> for $crate::eval::Value { + impl From<$type> for $crate::model::Value { fn from(v: $type) -> Self { - $crate::eval::Value::Dyn($crate::eval::Dynamic::new(v)) + $crate::model::Value::Dyn($crate::model::Dynamic::new(v)) } } }; @@ -41,12 +40,12 @@ macro_rules! dynamic { /// Make a type castable from a value. macro_rules! castable { ($type:ty: $inner:ty) => { - impl $crate::eval::Cast<$crate::eval::Value> for $type { - fn is(value: &$crate::eval::Value) -> bool { + impl $crate::model::Cast<$crate::model::Value> for $type { + fn is(value: &$crate::model::Value) -> bool { <$inner>::is(value) } - fn cast(value: $crate::eval::Value) -> $crate::diag::StrResult { + fn cast(value: $crate::model::Value) -> $crate::diag::StrResult { <$inner>::cast(value).map(Self) } } @@ -59,22 +58,22 @@ macro_rules! castable { $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* ) => { #[allow(unreachable_patterns)] - impl $crate::eval::Cast<$crate::eval::Value> for $type { - fn is(value: &$crate::eval::Value) -> bool { + impl $crate::model::Cast<$crate::model::Value> for $type { + fn is(value: &$crate::model::Value) -> bool { #[allow(unused_variables)] match value { $($pattern => true,)* - $crate::eval::Value::Dyn(dynamic) => { + $crate::model::Value::Dyn(dynamic) => { false $(|| dynamic.is::<$dyn_type>())* } _ => false, } } - fn cast(value: $crate::eval::Value) -> $crate::diag::StrResult { + fn cast(value: $crate::model::Value) -> $crate::diag::StrResult { let found = match value { $($pattern => return Ok($out),)* - $crate::eval::Value::Dyn(dynamic) => { + $crate::model::Value::Dyn(dynamic) => { $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() { return Ok($dyn_out); })* diff --git a/src/model/content.rs b/src/model/content.rs index 7828a3cdd..5f0536c3a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -21,21 +21,6 @@ use crate::library::text::{ use crate::util::EcoString; use crate::World; -/// Layout content into a collection of pages. -/// -/// Relayouts until all pinned locations are converged. -#[comemo::memoize] -pub fn layout(world: Tracked, content: &Content) -> SourceResult> { - let styles = StyleChain::with_root(&world.config().styles); - let scratch = Scratch::default(); - - let mut builder = Builder::new(world, &scratch, true); - builder.accept(content, styles)?; - - let (doc, shared) = builder.into_doc(styles)?; - doc.layout(world, shared) -} - /// Composable representation of styled content. /// /// This results from: @@ -332,7 +317,7 @@ impl Sum for Content { } /// Builds a document or a flow node from content. -struct Builder<'a> { +pub(super) struct Builder<'a> { /// The core context. world: Tracked<'a, dyn World>, /// Scratch arenas for building. @@ -349,7 +334,7 @@ struct Builder<'a> { /// Temporary storage arenas for building. #[derive(Default)] -struct Scratch<'a> { +pub(super) struct Scratch<'a> { /// An arena where intermediate style chains are stored. styles: Arena>, /// An arena where intermediate content resulting from show rules is stored. @@ -357,7 +342,11 @@ struct Scratch<'a> { } impl<'a> Builder<'a> { - fn new(world: Tracked<'a, dyn World>, scratch: &'a Scratch<'a>, top: bool) -> Self { + pub fn new( + world: Tracked<'a, dyn World>, + scratch: &'a Scratch<'a>, + top: bool, + ) -> Self { Self { world, scratch, @@ -368,7 +357,7 @@ impl<'a> Builder<'a> { } } - fn into_doc( + pub fn into_doc( mut self, styles: StyleChain<'a>, ) -> SourceResult<(DocNode, StyleChain<'a>)> { @@ -377,7 +366,7 @@ impl<'a> Builder<'a> { Ok((DocNode(pages), shared)) } - fn into_flow( + pub fn into_flow( mut self, styles: StyleChain<'a>, ) -> SourceResult<(FlowNode, StyleChain<'a>)> { @@ -386,7 +375,7 @@ impl<'a> Builder<'a> { Ok((FlowNode(children), shared)) } - fn accept( + pub fn accept( &mut self, content: &'a Content, styles: StyleChain<'a>, diff --git a/src/eval/dict.rs b/src/model/dict.rs similarity index 98% rename from src/eval/dict.rs rename to src/model/dict.rs index b95ead31c..3e4fd956c 100644 --- a/src/eval/dict.rs +++ b/src/model/dict.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use super::{Args, Array, Func, Str, Value, Vm}; use crate::diag::{SourceResult, StrResult}; -use crate::parse::is_ident; +use crate::syntax::is_ident; use crate::syntax::Spanned; use crate::util::ArcExt; @@ -16,7 +16,7 @@ macro_rules! dict { #[allow(unused_mut)] let mut map = std::collections::BTreeMap::new(); $(map.insert($key.into(), $value.into());)* - $crate::eval::Dict::from_map(map) + $crate::model::Dict::from_map(map) }}; } diff --git a/src/eval/mod.rs b/src/model/eval.rs similarity index 98% rename from src/eval/mod.rs rename to src/model/eval.rs index 0a3d6545a..aa5f0378a 100644 --- a/src/eval/mod.rs +++ b/src/model/eval.rs @@ -1,50 +1,20 @@ //! Evaluation of markup into modules. -#[macro_use] -mod cast; -#[macro_use] -mod array; -#[macro_use] -mod dict; -#[macro_use] -mod str; -#[macro_use] -mod value; -mod args; -mod capture; -mod func; -pub mod methods; -pub mod ops; -mod raw; -mod scope; -mod vm; - -pub use self::str::*; -pub use args::*; -pub use array::*; -pub use capture::*; -pub use cast::*; -pub use dict::*; -pub use func::*; -pub use raw::*; -pub use scope::*; -pub use typst_macros::node; -pub use value::*; -pub use vm::*; - use std::collections::BTreeMap; use std::sync::Arc; use comemo::{Track, Tracked}; use unicode_segmentation::UnicodeSegmentation; +use super::{ + methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, + Pattern, Recipe, Scope, Scopes, StyleEntry, StyleMap, Value, Vm, +}; use crate::diag::{At, SourceResult, StrResult, Trace, Tracepoint}; use crate::geom::{Angle, Em, Fraction, Length, Ratio}; use crate::library; -use crate::model::{Content, Pattern, Recipe, StyleEntry, StyleMap}; -use crate::source::SourceId; use crate::syntax::ast::TypedNode; -use crate::syntax::{ast, Span, Spanned, Unit}; +use crate::syntax::{ast, SourceId, Span, Spanned, Unit}; use crate::util::EcoString; use crate::World; diff --git a/src/eval/func.rs b/src/model/func.rs similarity index 98% rename from src/eval/func.rs rename to src/model/func.rs index c307b2373..a4f63aa1b 100644 --- a/src/eval/func.rs +++ b/src/model/func.rs @@ -4,11 +4,12 @@ use std::sync::Arc; use comemo::{Track, Tracked}; -use super::{Args, Eval, Flow, Route, Scope, Scopes, Value, Vm}; +use super::{ + Args, Content, Eval, Flow, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm, +}; use crate::diag::{SourceResult, StrResult}; -use crate::model::{Content, NodeId, StyleMap}; -use crate::source::SourceId; use crate::syntax::ast::Expr; +use crate::syntax::SourceId; use crate::util::EcoString; use crate::World; diff --git a/src/model/layout.rs b/src/model/layout.rs index 8064afffe..09888ba5d 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry}; +use super::{Builder, Content, RawAlign, RawLength, Scratch}; use crate::diag::SourceResult; -use crate::eval::{RawAlign, RawLength}; use crate::frame::{Element, Frame}; use crate::geom::{ Align, Geometry, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke, @@ -18,6 +18,21 @@ use crate::library::graphics::MoveNode; use crate::library::layout::{AlignNode, PadNode}; use crate::World; +/// Layout content into a collection of pages. +/// +/// Relayouts until all pinned locations are converged. +#[comemo::memoize] +pub fn layout(world: Tracked, content: &Content) -> SourceResult> { + let styles = StyleChain::with_root(&world.config().styles); + let scratch = Scratch::default(); + + let mut builder = Builder::new(world, &scratch, true); + builder.accept(content, styles)?; + + let (doc, shared) = builder.into_doc(styles)?; + doc.layout(world, shared) +} + /// A node that can be layouted into a sequence of regions. /// /// Layouting returns one frame per used region. diff --git a/src/eval/methods.rs b/src/model/methods.rs similarity index 100% rename from src/eval/methods.rs rename to src/model/methods.rs diff --git a/src/model/mod.rs b/src/model/mod.rs index 5c8b82c0b..0ea5cbdda 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,18 +1,50 @@ -//! Styled and structured representation of layoutable content. +//! Layout and computation model. #[macro_use] mod styles; mod collapse; mod content; +mod eval; mod layout; mod property; mod recipe; mod show; +#[macro_use] +mod cast; +#[macro_use] +mod array; +#[macro_use] +mod dict; +#[macro_use] +mod str; +#[macro_use] +mod value; +mod args; +mod capture; +mod func; +pub mod methods; +pub mod ops; +mod raw; +mod scope; +mod vm; +pub use self::str::*; +pub use args::*; +pub use array::*; +pub use capture::*; +pub use cast::*; pub use collapse::*; pub use content::*; +pub use dict::*; +pub use eval::*; +pub use func::*; pub use layout::*; pub use property::*; +pub use raw::*; pub use recipe::*; +pub use scope::*; pub use show::*; pub use styles::*; +pub use typst_macros::node; +pub use value::*; +pub use vm::*; diff --git a/src/eval/ops.rs b/src/model/ops.rs similarity index 98% rename from src/eval/ops.rs rename to src/model/ops.rs index 7e4653200..c521f7045 100644 --- a/src/eval/ops.rs +++ b/src/model/ops.rs @@ -5,7 +5,6 @@ use std::cmp::Ordering; use super::{RawAlign, RawLength, RawStroke, Regex, Smart, Value}; use crate::diag::StrResult; use crate::geom::{Numeric, Relative, Spec, SpecAxis}; -use crate::model; use Value::*; /// Bail with a type mismatch error. @@ -21,8 +20,8 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (a, None) => a, (None, b) => b, (Str(a), Str(b)) => Str(a + b), - (Str(a), Content(b)) => Content(model::Content::Text(a.into()) + b), - (Content(a), Str(b)) => Content(a + model::Content::Text(b.into())), + (Str(a), Content(b)) => Content(super::Content::Text(a.into()) + b), + (Content(a), Str(b)) => Content(a + super::Content::Text(b.into())), (Content(a), Content(b)) => Content(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), @@ -87,8 +86,8 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (Str(a), Str(b)) => Str(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Str(b)) => Content(a + model::Content::Text(b.into())), - (Str(a), Content(b)) => Content(model::Content::Text(a.into()) + b), + (Content(a), Str(b)) => Content(a + super::Content::Text(b.into())), + (Str(a), Content(b)) => Content(super::Content::Text(a.into()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), diff --git a/src/model/property.rs b/src/model/property.rs index ab4f02e35..ed2ab1d05 100644 --- a/src/model/property.rs +++ b/src/model/property.rs @@ -5,8 +5,7 @@ use std::sync::Arc; use comemo::Prehashed; -use super::{Interruption, NodeId, StyleChain}; -use crate::eval::{RawLength, Smart}; +use super::{Interruption, NodeId, RawLength, Smart, StyleChain}; use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec}; use crate::library::layout::PageNode; use crate::library::structure::{DescNode, EnumNode, ListNode}; diff --git a/src/eval/raw.rs b/src/model/raw.rs similarity index 99% rename from src/eval/raw.rs rename to src/model/raw.rs index 9cf346b1c..b40a88ec4 100644 --- a/src/eval/raw.rs +++ b/src/model/raw.rs @@ -2,12 +2,11 @@ use std::cmp::Ordering; use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, Div, Mul, Neg}; -use super::{Smart, Value}; +use super::{Fold, Resolve, Smart, StyleChain, Value}; use crate::geom::{ Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke, }; use crate::library::text::TextNode; -use crate::model::{Fold, Resolve, StyleChain}; /// The unresolved alignment representation. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/src/model/recipe.rs b/src/model/recipe.rs index 27b1be424..05ef07a63 100644 --- a/src/model/recipe.rs +++ b/src/model/recipe.rs @@ -2,9 +2,11 @@ use std::fmt::{self, Debug, Formatter}; use comemo::Tracked; -use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntry}; +use super::{ + Args, Content, Func, Interruption, NodeId, Regex, Show, ShowNode, StyleChain, + StyleEntry, Value, +}; use crate::diag::SourceResult; -use crate::eval::{Args, Func, Regex, Value}; use crate::library::structure::{DescNode, EnumNode, ListNode}; use crate::syntax::Spanned; use crate::World; diff --git a/src/eval/scope.rs b/src/model/scope.rs similarity index 100% rename from src/eval/scope.rs rename to src/model/scope.rs diff --git a/src/model/show.rs b/src/model/show.rs index b30b22643..bff694486 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -4,9 +4,8 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; -use super::{Content, NodeId, Selector, StyleChain}; +use super::{Content, Dict, NodeId, Selector, StyleChain}; use crate::diag::SourceResult; -use crate::eval::Dict; use crate::World; /// A node that can be realized given some styles. diff --git a/src/eval/str.rs b/src/model/str.rs similarity index 83% rename from src/eval/str.rs rename to src/model/str.rs index 9d2375d31..62b378451 100644 --- a/src/eval/str.rs +++ b/src/model/str.rs @@ -13,7 +13,7 @@ use crate::util::EcoString; #[allow(unused_macros)] macro_rules! format_str { ($($tts:tt)*) => {{ - $crate::eval::Str::from(format_eco!($($tts)*)) + $crate::model::Str::from(format_eco!($($tts)*)) }}; } @@ -76,69 +76,69 @@ impl Str { } /// Whether the given pattern exists in this string. - pub fn contains(&self, pattern: TextPattern) -> bool { + pub fn contains(&self, pattern: StrPattern) -> bool { match pattern { - TextPattern::Str(pat) => self.0.contains(pat.as_str()), - TextPattern::Regex(re) => re.is_match(self), + StrPattern::Str(pat) => self.0.contains(pat.as_str()), + StrPattern::Regex(re) => re.is_match(self), } } /// Whether this string begins with the given pattern. - pub fn starts_with(&self, pattern: TextPattern) -> bool { + pub fn starts_with(&self, pattern: StrPattern) -> bool { match pattern { - TextPattern::Str(pat) => self.0.starts_with(pat.as_str()), - TextPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), + StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), } } /// Whether this string ends with the given pattern. - pub fn ends_with(&self, pattern: TextPattern) -> bool { + pub fn ends_with(&self, pattern: StrPattern) -> bool { match pattern { - TextPattern::Str(pat) => self.0.ends_with(pat.as_str()), - TextPattern::Regex(re) => { + StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), + StrPattern::Regex(re) => { re.find_iter(self).last().map_or(false, |m| m.end() == self.0.len()) } } } /// The text of the pattern's first match in this string. - pub fn find(&self, pattern: TextPattern) -> Option { + pub fn find(&self, pattern: StrPattern) -> Option { match pattern { - TextPattern::Str(pat) => self.0.contains(pat.as_str()).then(|| pat), - TextPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), + StrPattern::Str(pat) => self.0.contains(pat.as_str()).then(|| pat), + StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), } } /// The position of the pattern's first match in this string. - pub fn position(&self, pattern: TextPattern) -> Option { + pub fn position(&self, pattern: StrPattern) -> Option { match pattern { - TextPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), - TextPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), + StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), + StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), } } /// The start and, text and capture groups (if any) of the first match of /// the pattern in this string. - pub fn match_(&self, pattern: TextPattern) -> Option { + pub fn match_(&self, pattern: StrPattern) -> Option { match pattern { - TextPattern::Str(pat) => { + StrPattern::Str(pat) => { self.0.match_indices(pat.as_str()).next().map(match_to_dict) } - TextPattern::Regex(re) => re.captures(self).map(captures_to_dict), + StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), } } /// The start, end, text and capture groups (if any) of all matches of the /// pattern in this string. - pub fn matches(&self, pattern: TextPattern) -> Array { + pub fn matches(&self, pattern: StrPattern) -> Array { match pattern { - TextPattern::Str(pat) => self + StrPattern::Str(pat) => self .0 .match_indices(pat.as_str()) .map(match_to_dict) .map(Value::Dict) .collect(), - TextPattern::Regex(re) => re + StrPattern::Regex(re) => re .captures_iter(self) .map(captures_to_dict) .map(Value::Dict) @@ -147,14 +147,14 @@ impl Str { } /// Split this string at whitespace or a specific pattern. - pub fn split(&self, pattern: Option) -> Array { + pub fn split(&self, pattern: Option) -> Array { let s = self.as_str(); match pattern { None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), - Some(TextPattern::Str(pat)) => { + Some(StrPattern::Str(pat)) => { s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() } - Some(TextPattern::Regex(re)) => { + Some(StrPattern::Regex(re)) => { re.split(s).map(|v| Value::Str(v.into())).collect() } } @@ -166,20 +166,20 @@ impl Str { /// pattern. pub fn trim( &self, - pattern: Option, - at: Option, + pattern: Option, + at: Option, repeat: bool, ) -> Self { - let mut start = matches!(at, Some(TextSide::Start) | None); - let end = matches!(at, Some(TextSide::End) | None); + let mut start = matches!(at, Some(StrSide::Start) | None); + let end = matches!(at, Some(StrSide::End) | None); let trimmed = match pattern { None => match at { None => self.0.trim(), - Some(TextSide::Start) => self.0.trim_start(), - Some(TextSide::End) => self.0.trim_end(), + Some(StrSide::Start) => self.0.trim_start(), + Some(StrSide::End) => self.0.trim_end(), }, - Some(TextPattern::Str(pat)) => { + Some(StrPattern::Str(pat)) => { let pat = pat.as_str(); let mut s = self.as_str(); if repeat { @@ -199,7 +199,7 @@ impl Str { } s } - Some(TextPattern::Regex(re)) => { + Some(StrPattern::Regex(re)) => { let s = self.as_str(); let mut last = 0; let mut range = 0 .. s.len(); @@ -239,18 +239,13 @@ impl Str { /// Replace at most `count` occurances of the given pattern with a /// replacement string (beginning from the start). - pub fn replace( - &self, - pattern: TextPattern, - with: Self, - count: Option, - ) -> Self { + pub fn replace(&self, pattern: StrPattern, with: Self, count: Option) -> Self { match pattern { - TextPattern::Str(pat) => match count { + StrPattern::Str(pat) => match count { Some(n) => self.0.replacen(pat.as_str(), &with, n).into(), None => self.0.replace(pat.as_str(), &with).into(), }, - TextPattern::Regex(re) => match count { + StrPattern::Regex(re) => match count { Some(n) => re.replacen(self, n, with.as_str()).into(), None => re.replace(self, with.as_str()).into(), }, @@ -440,7 +435,7 @@ impl Hash for Regex { /// A pattern which can be searched for in a string. #[derive(Debug, Clone)] -pub enum TextPattern { +pub enum StrPattern { /// Just a string. Str(Str), /// A regular expression. @@ -448,7 +443,7 @@ pub enum TextPattern { } castable! { - TextPattern, + StrPattern, Expected: "string or regular expression", Value::Str(text) => Self::Str(text), @regex: Regex => Self::Regex(regex.clone()), @@ -456,7 +451,7 @@ castable! { /// A side of a string. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum TextSide { +pub enum StrSide { /// The logical start of the string, may be left or right depending on the /// language. Start, @@ -465,7 +460,7 @@ pub enum TextSide { } castable! { - TextSide, + StrSide, Expected: "start or end", @align: RawAlign => match align { RawAlign::Start => Self::Start, diff --git a/src/model/styles.rs b/src/model/styles.rs index 93b615fc1..76199ca11 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -107,7 +107,7 @@ impl StyleMap { /// Mark all contained properties as _scoped_. This means that they only /// apply to the first descendant node (of their type) in the hierarchy and /// not its children, too. This is used by - /// [constructors](crate::eval::Node::construct). + /// [constructors](super::Node::construct). pub fn scoped(mut self) -> Self { for entry in &mut self.0 { if let StyleEntry::Property(property) = entry { diff --git a/src/eval/value.rs b/src/model/value.rs similarity index 97% rename from src/eval/value.rs rename to src/model/value.rs index b7bd6d3c6..4075ce9cb 100644 --- a/src/eval/value.rs +++ b/src/model/value.rs @@ -4,11 +4,12 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use super::{ops, Args, Array, Cast, Dict, Func, RawLength, Str}; +use siphasher::sip128::{Hasher128, SipHasher}; + +use super::{ops, Args, Array, Cast, Content, Dict, Func, Layout, RawLength, Str}; use crate::diag::StrResult; use crate::geom::{Angle, Color, Em, Fraction, Length, Ratio, Relative, RgbaColor}; use crate::library::text::RawNode; -use crate::model::{Content, Layout}; use crate::util::EcoString; /// A computational value. @@ -296,7 +297,7 @@ trait Bounds: Debug + Sync + Send + 'static { fn as_any(&self) -> &dyn Any; fn dyn_eq(&self, other: &Dynamic) -> bool; fn dyn_type_name(&self) -> &'static str; - fn hash64(&self) -> u64; + fn hash128(&self) -> u128; } impl Bounds for T @@ -319,19 +320,19 @@ where T::TYPE_NAME } - fn hash64(&self) -> u64 { + fn hash128(&self) -> u128 { // Also hash the TypeId since nodes with different types but // equal data should be different. - let mut state = fxhash::FxHasher64::default(); + let mut state = SipHasher::new(); self.type_id().hash(&mut state); self.hash(&mut state); - state.finish() + state.finish128().as_u128() } } impl Hash for dyn Bounds { fn hash(&self, state: &mut H) { - state.write_u64(self.hash64()); + state.write_u128(self.hash128()); } } diff --git a/src/eval/vm.rs b/src/model/vm.rs similarity index 97% rename from src/eval/vm.rs rename to src/model/vm.rs index 0604e7bef..a1b1ba81c 100644 --- a/src/eval/vm.rs +++ b/src/model/vm.rs @@ -4,8 +4,7 @@ use comemo::Tracked; use super::{Route, Scopes, Value}; use crate::diag::{SourceError, StrResult}; -use crate::source::SourceId; -use crate::syntax::Span; +use crate::syntax::{SourceId, Span}; use crate::util::PathExt; use crate::World; diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index bfb360785..325b72742 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -6,7 +6,7 @@ use std::ops::Range; use syntect::highlighting::{Color, FontStyle, Highlighter, Style, Theme}; use syntect::parsing::Scope; -use super::{NodeKind, SyntaxNode}; +use super::{parse, NodeKind, SyntaxNode}; /// Highlight source text into a standalone HTML document. pub fn highlight_html(text: &str, theme: &Theme) -> String { @@ -28,7 +28,7 @@ pub fn highlight_pre(text: &str, theme: &Theme) -> String { let mut buf = String::new(); buf.push_str("
\n");
 
-    let root = crate::parse::parse(text);
+    let root = parse(text);
     highlight_themed(&root, theme, |range, style| {
         let styled = style != Style::default();
         if styled {
@@ -401,8 +401,8 @@ impl Category {
 
 #[cfg(test)]
 mod tests {
+    use super::super::Source;
     use super::*;
-    use crate::source::Source;
 
     #[test]
     fn test_highlighting() {
diff --git a/src/parse/incremental.rs b/src/syntax/incremental.rs
similarity index 99%
rename from src/parse/incremental.rs
rename to src/syntax/incremental.rs
index 4651a7843..529defd76 100644
--- a/src/parse/incremental.rs
+++ b/src/syntax/incremental.rs
@@ -1,10 +1,9 @@
 use std::ops::Range;
 use std::sync::Arc;
 
-use crate::syntax::{InnerNode, NodeKind, Span, SyntaxNode};
-
 use super::{
-    is_newline, parse, reparse_code_block, reparse_content_block, reparse_markup_elements,
+    is_newline, parse, reparse_code_block, reparse_content_block,
+    reparse_markup_elements, InnerNode, NodeKind, Span, SyntaxNode,
 };
 
 /// Refresh the given syntax node with as little parsing as possible.
@@ -413,9 +412,8 @@ fn next_at_start(kind: &NodeKind, prev: bool) -> bool {
 #[rustfmt::skip]
 mod tests {
     use super::*;
-    use crate::parse::parse;
-    use crate::parse::tests::check;
-    use crate::source::Source;
+    use super::super::{parse, Source};
+    use super::super::tests::check;
 
     #[track_caller]
     fn test(prev: &str, range: Range, with: &str, goal: Range) {
diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs
index 8b172defd..1a23db5f9 100644
--- a/src/syntax/mod.rs
+++ b/src/syntax/mod.rs
@@ -1,558 +1,41 @@
-//! Syntax types.
+//! Syntax definition, parsing, and highlighting.
 
 pub mod ast;
 pub mod highlight;
+mod incremental;
 mod kind;
+mod node;
+mod parser;
+mod parsing;
+mod resolve;
+mod source;
 mod span;
-
-use std::fmt::{self, Debug, Formatter};
-use std::ops::Range;
-use std::sync::Arc;
+mod tokens;
 
 pub use kind::*;
+pub use node::*;
+pub use parsing::*;
+pub use source::*;
 pub use span::*;
+pub use tokens::*;
 
-use self::ast::TypedNode;
-use crate::diag::SourceError;
-use crate::source::SourceId;
+use incremental::reparse;
+use parser::*;
 
-/// An inner or leaf node in the untyped syntax tree.
-#[derive(Clone, PartialEq, Hash)]
-pub enum SyntaxNode {
-    /// A reference-counted inner node.
-    Inner(Arc),
-    /// A leaf token.
-    Leaf(NodeData),
-}
+#[cfg(test)]
+mod tests {
+    use std::fmt::Debug;
 
-impl SyntaxNode {
-    /// The metadata of the node.
-    pub fn data(&self) -> &NodeData {
-        match self {
-            Self::Inner(inner) => &inner.data,
-            Self::Leaf(leaf) => leaf,
-        }
-    }
-
-    /// The type of the node.
-    pub fn kind(&self) -> &NodeKind {
-        self.data().kind()
-    }
-
-    /// The length of the node.
-    pub fn len(&self) -> usize {
-        self.data().len()
-    }
-
-    /// The number of descendants, including the node itself.
-    pub fn descendants(&self) -> usize {
-        match self {
-            Self::Inner(inner) => inner.descendants(),
-            Self::Leaf(_) => 1,
-        }
-    }
-
-    /// The span of the node.
-    pub fn span(&self) -> Span {
-        self.data().span()
-    }
-
-    /// Whether the node or its children contain an error.
-    pub fn erroneous(&self) -> bool {
-        match self {
-            Self::Inner(node) => node.erroneous,
-            Self::Leaf(data) => data.kind.is_error(),
-        }
-    }
-
-    /// The error messages for this node and its descendants.
-    pub fn errors(&self) -> Vec {
-        if !self.erroneous() {
-            return vec![];
-        }
-
-        match self.kind() {
-            NodeKind::Error(pos, message) => {
-                vec![SourceError::new(self.span(), message.clone()).with_pos(*pos)]
-            }
-            _ => self
-                .children()
-                .filter(|node| node.erroneous())
-                .flat_map(|node| node.errors())
-                .collect(),
-        }
-    }
-
-    /// The node's children.
-    pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> {
-        match self {
-            Self::Inner(inner) => inner.children(),
-            Self::Leaf(_) => [].iter(),
-        }
-    }
-
-    /// Convert the node to a typed AST node.
-    pub fn cast(&self) -> Option
+    #[track_caller]
+    pub fn check(text: &str, found: T, expected: T)
     where
-        T: TypedNode,
+        T: Debug + PartialEq,
     {
-        T::from_untyped(self)
-    }
-
-    /// Get the first child that can cast to the AST type `T`.
-    pub fn cast_first_child(&self) -> Option {
-        self.children().find_map(Self::cast)
-    }
-
-    /// Get the last child that can cast to the AST type `T`.
-    pub fn cast_last_child(&self) -> Option {
-        self.children().rev().find_map(Self::cast)
-    }
-
-    /// Change the type of the node.
-    pub fn convert(&mut self, kind: NodeKind) {
-        match self {
-            Self::Inner(inner) => {
-                let node = Arc::make_mut(inner);
-                node.erroneous |= kind.is_error();
-                node.data.kind = kind;
-            }
-            Self::Leaf(leaf) => leaf.kind = kind,
-        }
-    }
-
-    /// Set a synthetic span for the node and all its descendants.
-    pub fn synthesize(&mut self, span: Span) {
-        match self {
-            Self::Inner(inner) => Arc::make_mut(inner).synthesize(span),
-            Self::Leaf(leaf) => leaf.synthesize(span),
-        }
-    }
-
-    /// Assign spans to each node.
-    pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult {
-        match self {
-            Self::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within),
-            Self::Leaf(leaf) => leaf.numberize(id, within),
-        }
-    }
-
-    /// The upper bound of assigned numbers in this subtree.
-    pub fn upper(&self) -> u64 {
-        match self {
-            Self::Inner(inner) => inner.upper(),
-            Self::Leaf(leaf) => leaf.span().number() + 1,
-        }
-    }
-
-    /// If the span points into this node, convert it to a byte range.
-    pub fn range(&self, span: Span, offset: usize) -> Option> {
-        match self {
-            Self::Inner(inner) => inner.range(span, offset),
-            Self::Leaf(leaf) => leaf.range(span, offset),
-        }
-    }
-
-    /// Returns all leaf descendants of this node (may include itself).
-    ///
-    /// This method is slow and only intended for testing.
-    pub fn leafs(&self) -> Vec {
-        if match self {
-            Self::Inner(inner) => inner.children.is_empty(),
-            Self::Leaf(_) => true,
-        } {
-            vec![self.clone()]
-        } else {
-            self.children().flat_map(Self::leafs).collect()
+        if found != expected {
+            println!("source:   {text:?}");
+            println!("expected: {expected:#?}");
+            println!("found:    {found:#?}");
+            panic!("test failed");
         }
     }
 }
-
-impl Default for SyntaxNode {
-    fn default() -> Self {
-        Self::Leaf(NodeData::new(NodeKind::None, 0))
-    }
-}
-
-impl Debug for SyntaxNode {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        match self {
-            Self::Inner(node) => node.fmt(f),
-            Self::Leaf(token) => token.fmt(f),
-        }
-    }
-}
-
-/// An inner node in the untyped syntax tree.
-#[derive(Clone, Hash)]
-pub struct InnerNode {
-    /// Node metadata.
-    data: NodeData,
-    /// The number of nodes in the whole subtree, including this node.
-    descendants: usize,
-    /// Whether this node or any of its children are erroneous.
-    erroneous: bool,
-    /// The upper bound of this node's numbering range.
-    upper: u64,
-    /// This node's children, losslessly make up this node.
-    children: Vec,
-}
-
-impl InnerNode {
-    /// Creates a new node with the given kind and a single child.
-    pub fn with_child(kind: NodeKind, child: impl Into) -> Self {
-        Self::with_children(kind, vec![child.into()])
-    }
-
-    /// 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 descendants = 1;
-        let mut erroneous = kind.is_error();
-
-        for child in &children {
-            len += child.len();
-            descendants += child.descendants();
-            erroneous |= child.erroneous();
-        }
-
-        Self {
-            data: NodeData::new(kind, len),
-            descendants,
-            erroneous,
-            upper: 0,
-            children,
-        }
-    }
-
-    /// The node's metadata.
-    pub fn data(&self) -> &NodeData {
-        &self.data
-    }
-
-    /// The node's type.
-    pub fn kind(&self) -> &NodeKind {
-        self.data().kind()
-    }
-
-    /// The node's length.
-    pub fn len(&self) -> usize {
-        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 descendants(&self) -> usize {
-        self.descendants
-    }
-
-    /// 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 descendants.
-    pub fn synthesize(&mut self, span: Span) {
-        self.data.synthesize(span);
-        for child in &mut self.children {
-            child.synthesize(span);
-        }
-    }
-
-    /// Assign span numbers `within` an interval to this node's subtree or just
-    /// a `range` of its children.
-    pub fn numberize(
-        &mut self,
-        id: SourceId,
-        range: Option>,
-        within: Range,
-    ) -> NumberingResult {
-        // Determine how many nodes we will number.
-        let descendants = match &range {
-            Some(range) if range.is_empty() => return Ok(()),
-            Some(range) => self.children[range.clone()]
-                .iter()
-                .map(SyntaxNode::descendants)
-                .sum::(),
-            None => self.descendants,
-        };
-
-        // Determine the distance between two neighbouring assigned numbers. If
-        // possible, we try to fit all numbers into the left half of `within`
-        // so that there is space for future insertions.
-        let space = within.end - within.start;
-        let mut stride = space / (2 * descendants as u64);
-        if stride == 0 {
-            stride = space / self.descendants as u64;
-            if stride == 0 {
-                return Err(Unnumberable);
-            }
-        }
-
-        // Number this node itself.
-        let mut start = within.start;
-        if range.is_none() {
-            let end = start + stride;
-            self.data.numberize(id, start .. end)?;
-            self.upper = within.end;
-            start = end;
-        }
-
-        // Number the children.
-        let len = self.children.len();
-        for child in &mut self.children[range.unwrap_or(0 .. len)] {
-            let end = start + child.descendants() as u64 * stride;
-            child.numberize(id, start .. end)?;
-            start = end;
-        }
-
-        Ok(())
-    }
-
-    /// The upper bound of assigned numbers in this subtree.
-    pub fn upper(&self) -> u64 {
-        self.upper
-    }
-
-    /// If the span points into this node, convert it to a byte range.
-    pub fn range(&self, span: Span, mut offset: usize) -> Option> {
-        // Check whether we found it.
-        if let Some(range) = self.data.range(span, offset) {
-            return Some(range);
-        }
-
-        // The parent of a subtree has a smaller span number than all of its
-        // descendants. Therefore, we can bail out early if the target span's
-        // number is smaller than our number.
-        if span.number() < self.span().number() {
-            return None;
-        }
-
-        let mut children = self.children.iter().peekable();
-        while let Some(child) = children.next() {
-            // Every node in this child's subtree has a smaller span number than
-            // the next sibling. Therefore we only need to recurse if the next
-            // sibling's span number is larger than the target span's number.
-            if children
-                .peek()
-                .map_or(true, |next| next.span().number() > span.number())
-            {
-                if let Some(range) = child.range(span, offset) {
-                    return Some(range);
-                }
-            }
-
-            offset += child.len();
-        }
-
-        None
-    }
-
-    /// The node's children, mutably.
-    pub(crate) fn children_mut(&mut self) -> &mut [SyntaxNode] {
-        &mut self.children
-    }
-
-    /// Replaces a range of children with a replacement.
-    ///
-    /// May have mutated the children if it returns `Err(_)`.
-    pub(crate) fn replace_children(
-        &mut self,
-        mut range: Range,
-        replacement: Vec,
-    ) -> NumberingResult {
-        let superseded = &self.children[range.clone()];
-
-        // 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 number of descendants.
-        self.descendants = self.descendants
-            + replacement.iter().map(SyntaxNode::descendants).sum::()
-            - superseded.iter().map(SyntaxNode::descendants).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.
-        let replacement_count = replacement.len();
-        self.children.splice(range.clone(), replacement);
-        range.end = range.start + replacement_count;
-
-        // Renumber the new children. Retries until it works, taking
-        // exponentially more children into account.
-        let mut left = 0;
-        let mut right = 0;
-        let max_left = range.start;
-        let max_right = self.children.len() - range.end;
-        loop {
-            let renumber = range.start - left .. range.end + right;
-
-            // The minimum assignable number is either
-            // - the upper bound of the node right before the to-be-renumbered
-            //   children,
-            // - or this inner node's span number plus one if renumbering starts
-            //   at the first child.
-            let start_number = renumber
-                .start
-                .checked_sub(1)
-                .and_then(|i| self.children.get(i))
-                .map_or(self.span().number() + 1, |child| child.upper());
-
-            // The upper bound for renumbering is either
-            // - the span number of the first child after the to-be-renumbered
-            //   children,
-            // - or this node's upper bound if renumbering ends behind the last
-            //   child.
-            let end_number = self
-                .children
-                .get(renumber.end)
-                .map_or(self.upper(), |next| next.span().number());
-
-            // Try to renumber.
-            let within = start_number .. end_number;
-            let id = self.span().source();
-            if self.numberize(id, Some(renumber), within).is_ok() {
-                return Ok(());
-            }
-
-            // If it didn't even work with all children, we give up.
-            if left == max_left && right == max_right {
-                return Err(Unnumberable);
-            }
-
-            // Exponential expansion to both sides.
-            left = (left + 1).next_power_of_two().min(max_left);
-            right = (right + 1).next_power_of_two().min(max_right);
-        }
-    }
-
-    /// Update this node after changes were made to one of its children.
-    pub(crate) fn update_parent(
-        &mut self,
-        prev_len: usize,
-        new_len: usize,
-        prev_descendants: usize,
-        new_descendants: usize,
-    ) {
-        self.data.len = self.data.len + new_len - prev_len;
-        self.descendants = self.descendants + new_descendants - prev_descendants;
-        self.erroneous = self.children.iter().any(SyntaxNode::erroneous);
-    }
-}
-
-impl From for SyntaxNode {
-    fn from(node: InnerNode) -> Self {
-        Arc::new(node).into()
-    }
-}
-
-impl From> for SyntaxNode {
-    fn from(node: Arc) -> Self {
-        Self::Inner(node)
-    }
-}
-
-impl Debug for InnerNode {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        self.data.fmt(f)?;
-        if !self.children.is_empty() {
-            f.write_str(" ")?;
-            f.debug_list().entries(&self.children).finish()?;
-        }
-        Ok(())
-    }
-}
-
-impl PartialEq for InnerNode {
-    fn eq(&self, other: &Self) -> bool {
-        self.data == other.data
-            && self.descendants == other.descendants
-            && self.erroneous == other.erroneous
-            && self.children == other.children
-    }
-}
-
-/// Data shared between inner and leaf nodes.
-#[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, span: Span::detached() }
-    }
-
-    /// The node's type.
-    pub fn kind(&self) -> &NodeKind {
-        &self.kind
-    }
-
-    /// The node's length.
-    pub fn len(&self) -> usize {
-        self.len
-    }
-
-    /// The node's span.
-    pub fn span(&self) -> Span {
-        self.span
-    }
-
-    /// Set a synthetic span for the node.
-    pub fn synthesize(&mut self, span: Span) {
-        self.span = span;
-    }
-
-    /// Assign a span to the node.
-    pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult {
-        if within.start < within.end {
-            self.span = Span::new(id, (within.start + within.end) / 2);
-            Ok(())
-        } else {
-            Err(Unnumberable)
-        }
-    }
-
-    /// 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())
-    }
-}
-
-impl From for SyntaxNode {
-    fn from(token: NodeData) -> Self {
-        Self::Leaf(token)
-    }
-}
-
-impl Debug for NodeData {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        write!(f, "{:?}: {}", self.kind, self.len)
-    }
-}
-
-impl PartialEq for NodeData {
-    fn eq(&self, other: &Self) -> bool {
-        self.kind == other.kind && self.len == other.len
-    }
-}
diff --git a/src/syntax/node.rs b/src/syntax/node.rs
new file mode 100644
index 000000000..6a7d424aa
--- /dev/null
+++ b/src/syntax/node.rs
@@ -0,0 +1,548 @@
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Range;
+use std::sync::Arc;
+
+use super::ast::TypedNode;
+use super::{NodeKind, NumberingResult, SourceId, Span, Unnumberable};
+use crate::diag::SourceError;
+
+/// An inner or leaf node in the untyped syntax tree.
+#[derive(Clone, PartialEq, Hash)]
+pub enum SyntaxNode {
+    /// A reference-counted inner node.
+    Inner(Arc),
+    /// A leaf token.
+    Leaf(NodeData),
+}
+
+impl SyntaxNode {
+    /// The metadata of the node.
+    pub fn data(&self) -> &NodeData {
+        match self {
+            Self::Inner(inner) => &inner.data,
+            Self::Leaf(leaf) => leaf,
+        }
+    }
+
+    /// The type of the node.
+    pub fn kind(&self) -> &NodeKind {
+        self.data().kind()
+    }
+
+    /// The length of the node.
+    pub fn len(&self) -> usize {
+        self.data().len()
+    }
+
+    /// The number of descendants, including the node itself.
+    pub fn descendants(&self) -> usize {
+        match self {
+            Self::Inner(inner) => inner.descendants(),
+            Self::Leaf(_) => 1,
+        }
+    }
+
+    /// The span of the node.
+    pub fn span(&self) -> Span {
+        self.data().span()
+    }
+
+    /// Whether the node or its children contain an error.
+    pub fn erroneous(&self) -> bool {
+        match self {
+            Self::Inner(node) => node.erroneous,
+            Self::Leaf(data) => data.kind.is_error(),
+        }
+    }
+
+    /// The error messages for this node and its descendants.
+    pub fn errors(&self) -> Vec {
+        if !self.erroneous() {
+            return vec![];
+        }
+
+        match self.kind() {
+            NodeKind::Error(pos, message) => {
+                vec![SourceError::new(self.span(), message.clone()).with_pos(*pos)]
+            }
+            _ => self
+                .children()
+                .filter(|node| node.erroneous())
+                .flat_map(|node| node.errors())
+                .collect(),
+        }
+    }
+
+    /// The node's children.
+    pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> {
+        match self {
+            Self::Inner(inner) => inner.children(),
+            Self::Leaf(_) => [].iter(),
+        }
+    }
+
+    /// Convert the node to a typed AST node.
+    pub fn cast(&self) -> Option
+    where
+        T: TypedNode,
+    {
+        T::from_untyped(self)
+    }
+
+    /// Get the first child that can cast to the AST type `T`.
+    pub fn cast_first_child(&self) -> Option {
+        self.children().find_map(Self::cast)
+    }
+
+    /// Get the last child that can cast to the AST type `T`.
+    pub fn cast_last_child(&self) -> Option {
+        self.children().rev().find_map(Self::cast)
+    }
+
+    /// Change the type of the node.
+    pub fn convert(&mut self, kind: NodeKind) {
+        match self {
+            Self::Inner(inner) => {
+                let node = Arc::make_mut(inner);
+                node.erroneous |= kind.is_error();
+                node.data.kind = kind;
+            }
+            Self::Leaf(leaf) => leaf.kind = kind,
+        }
+    }
+
+    /// Set a synthetic span for the node and all its descendants.
+    pub fn synthesize(&mut self, span: Span) {
+        match self {
+            Self::Inner(inner) => Arc::make_mut(inner).synthesize(span),
+            Self::Leaf(leaf) => leaf.synthesize(span),
+        }
+    }
+
+    /// Assign spans to each node.
+    pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult {
+        match self {
+            Self::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within),
+            Self::Leaf(leaf) => leaf.numberize(id, within),
+        }
+    }
+
+    /// The upper bound of assigned numbers in this subtree.
+    pub fn upper(&self) -> u64 {
+        match self {
+            Self::Inner(inner) => inner.upper(),
+            Self::Leaf(leaf) => leaf.span().number() + 1,
+        }
+    }
+
+    /// If the span points into this node, convert it to a byte range.
+    pub fn range(&self, span: Span, offset: usize) -> Option> {
+        match self {
+            Self::Inner(inner) => inner.range(span, offset),
+            Self::Leaf(leaf) => leaf.range(span, offset),
+        }
+    }
+
+    /// Returns all leaf descendants of this node (may include itself).
+    ///
+    /// This method is slow and only intended for testing.
+    pub fn leafs(&self) -> Vec {
+        if match self {
+            Self::Inner(inner) => inner.children.is_empty(),
+            Self::Leaf(_) => true,
+        } {
+            vec![self.clone()]
+        } else {
+            self.children().flat_map(Self::leafs).collect()
+        }
+    }
+}
+
+impl Default for SyntaxNode {
+    fn default() -> Self {
+        Self::Leaf(NodeData::new(NodeKind::None, 0))
+    }
+}
+
+impl Debug for SyntaxNode {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self {
+            Self::Inner(node) => node.fmt(f),
+            Self::Leaf(token) => token.fmt(f),
+        }
+    }
+}
+
+/// An inner node in the untyped syntax tree.
+#[derive(Clone, Hash)]
+pub struct InnerNode {
+    /// Node metadata.
+    data: NodeData,
+    /// The number of nodes in the whole subtree, including this node.
+    descendants: usize,
+    /// Whether this node or any of its children are erroneous.
+    erroneous: bool,
+    /// The upper bound of this node's numbering range.
+    upper: u64,
+    /// This node's children, losslessly make up this node.
+    children: Vec,
+}
+
+impl InnerNode {
+    /// Creates a new node with the given kind and a single child.
+    pub fn with_child(kind: NodeKind, child: impl Into) -> Self {
+        Self::with_children(kind, vec![child.into()])
+    }
+
+    /// 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 descendants = 1;
+        let mut erroneous = kind.is_error();
+
+        for child in &children {
+            len += child.len();
+            descendants += child.descendants();
+            erroneous |= child.erroneous();
+        }
+
+        Self {
+            data: NodeData::new(kind, len),
+            descendants,
+            erroneous,
+            upper: 0,
+            children,
+        }
+    }
+
+    /// The node's metadata.
+    pub fn data(&self) -> &NodeData {
+        &self.data
+    }
+
+    /// The node's type.
+    pub fn kind(&self) -> &NodeKind {
+        self.data().kind()
+    }
+
+    /// The node's length.
+    pub fn len(&self) -> usize {
+        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 descendants(&self) -> usize {
+        self.descendants
+    }
+
+    /// 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 descendants.
+    pub fn synthesize(&mut self, span: Span) {
+        self.data.synthesize(span);
+        for child in &mut self.children {
+            child.synthesize(span);
+        }
+    }
+
+    /// Assign span numbers `within` an interval to this node's subtree or just
+    /// a `range` of its children.
+    pub fn numberize(
+        &mut self,
+        id: SourceId,
+        range: Option>,
+        within: Range,
+    ) -> NumberingResult {
+        // Determine how many nodes we will number.
+        let descendants = match &range {
+            Some(range) if range.is_empty() => return Ok(()),
+            Some(range) => self.children[range.clone()]
+                .iter()
+                .map(SyntaxNode::descendants)
+                .sum::(),
+            None => self.descendants,
+        };
+
+        // Determine the distance between two neighbouring assigned numbers. If
+        // possible, we try to fit all numbers into the left half of `within`
+        // so that there is space for future insertions.
+        let space = within.end - within.start;
+        let mut stride = space / (2 * descendants as u64);
+        if stride == 0 {
+            stride = space / self.descendants as u64;
+            if stride == 0 {
+                return Err(Unnumberable);
+            }
+        }
+
+        // Number this node itself.
+        let mut start = within.start;
+        if range.is_none() {
+            let end = start + stride;
+            self.data.numberize(id, start .. end)?;
+            self.upper = within.end;
+            start = end;
+        }
+
+        // Number the children.
+        let len = self.children.len();
+        for child in &mut self.children[range.unwrap_or(0 .. len)] {
+            let end = start + child.descendants() as u64 * stride;
+            child.numberize(id, start .. end)?;
+            start = end;
+        }
+
+        Ok(())
+    }
+
+    /// The upper bound of assigned numbers in this subtree.
+    pub fn upper(&self) -> u64 {
+        self.upper
+    }
+
+    /// If the span points into this node, convert it to a byte range.
+    pub fn range(&self, span: Span, mut offset: usize) -> Option> {
+        // Check whether we found it.
+        if let Some(range) = self.data.range(span, offset) {
+            return Some(range);
+        }
+
+        // The parent of a subtree has a smaller span number than all of its
+        // descendants. Therefore, we can bail out early if the target span's
+        // number is smaller than our number.
+        if span.number() < self.span().number() {
+            return None;
+        }
+
+        let mut children = self.children.iter().peekable();
+        while let Some(child) = children.next() {
+            // Every node in this child's subtree has a smaller span number than
+            // the next sibling. Therefore we only need to recurse if the next
+            // sibling's span number is larger than the target span's number.
+            if children
+                .peek()
+                .map_or(true, |next| next.span().number() > span.number())
+            {
+                if let Some(range) = child.range(span, offset) {
+                    return Some(range);
+                }
+            }
+
+            offset += child.len();
+        }
+
+        None
+    }
+
+    /// The node's children, mutably.
+    pub(crate) fn children_mut(&mut self) -> &mut [SyntaxNode] {
+        &mut self.children
+    }
+
+    /// Replaces a range of children with a replacement.
+    ///
+    /// May have mutated the children if it returns `Err(_)`.
+    pub(crate) fn replace_children(
+        &mut self,
+        mut range: Range,
+        replacement: Vec,
+    ) -> NumberingResult {
+        let superseded = &self.children[range.clone()];
+
+        // 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 number of descendants.
+        self.descendants = self.descendants
+            + replacement.iter().map(SyntaxNode::descendants).sum::()
+            - superseded.iter().map(SyntaxNode::descendants).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.
+        let replacement_count = replacement.len();
+        self.children.splice(range.clone(), replacement);
+        range.end = range.start + replacement_count;
+
+        // Renumber the new children. Retries until it works, taking
+        // exponentially more children into account.
+        let mut left = 0;
+        let mut right = 0;
+        let max_left = range.start;
+        let max_right = self.children.len() - range.end;
+        loop {
+            let renumber = range.start - left .. range.end + right;
+
+            // The minimum assignable number is either
+            // - the upper bound of the node right before the to-be-renumbered
+            //   children,
+            // - or this inner node's span number plus one if renumbering starts
+            //   at the first child.
+            let start_number = renumber
+                .start
+                .checked_sub(1)
+                .and_then(|i| self.children.get(i))
+                .map_or(self.span().number() + 1, |child| child.upper());
+
+            // The upper bound for renumbering is either
+            // - the span number of the first child after the to-be-renumbered
+            //   children,
+            // - or this node's upper bound if renumbering ends behind the last
+            //   child.
+            let end_number = self
+                .children
+                .get(renumber.end)
+                .map_or(self.upper(), |next| next.span().number());
+
+            // Try to renumber.
+            let within = start_number .. end_number;
+            let id = self.span().source();
+            if self.numberize(id, Some(renumber), within).is_ok() {
+                return Ok(());
+            }
+
+            // If it didn't even work with all children, we give up.
+            if left == max_left && right == max_right {
+                return Err(Unnumberable);
+            }
+
+            // Exponential expansion to both sides.
+            left = (left + 1).next_power_of_two().min(max_left);
+            right = (right + 1).next_power_of_two().min(max_right);
+        }
+    }
+
+    /// Update this node after changes were made to one of its children.
+    pub(crate) fn update_parent(
+        &mut self,
+        prev_len: usize,
+        new_len: usize,
+        prev_descendants: usize,
+        new_descendants: usize,
+    ) {
+        self.data.len = self.data.len + new_len - prev_len;
+        self.descendants = self.descendants + new_descendants - prev_descendants;
+        self.erroneous = self.children.iter().any(SyntaxNode::erroneous);
+    }
+}
+
+impl From for SyntaxNode {
+    fn from(node: InnerNode) -> Self {
+        Arc::new(node).into()
+    }
+}
+
+impl From> for SyntaxNode {
+    fn from(node: Arc) -> Self {
+        Self::Inner(node)
+    }
+}
+
+impl Debug for InnerNode {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        self.data.fmt(f)?;
+        if !self.children.is_empty() {
+            f.write_str(" ")?;
+            f.debug_list().entries(&self.children).finish()?;
+        }
+        Ok(())
+    }
+}
+
+impl PartialEq for InnerNode {
+    fn eq(&self, other: &Self) -> bool {
+        self.data == other.data
+            && self.descendants == other.descendants
+            && self.erroneous == other.erroneous
+            && self.children == other.children
+    }
+}
+
+/// Data shared between inner and leaf nodes.
+#[derive(Clone, Hash)]
+pub struct NodeData {
+    /// What kind of node this is (each kind would have its own struct in a
+    /// strongly typed AST).
+    pub(super) 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, span: Span::detached() }
+    }
+
+    /// The node's type.
+    pub fn kind(&self) -> &NodeKind {
+        &self.kind
+    }
+
+    /// The node's length.
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    /// The node's span.
+    pub fn span(&self) -> Span {
+        self.span
+    }
+
+    /// Set a synthetic span for the node.
+    pub fn synthesize(&mut self, span: Span) {
+        self.span = span;
+    }
+
+    /// Assign a span to the node.
+    pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult {
+        if within.start < within.end {
+            self.span = Span::new(id, (within.start + within.end) / 2);
+            Ok(())
+        } else {
+            Err(Unnumberable)
+        }
+    }
+
+    /// 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())
+    }
+}
+
+impl From for SyntaxNode {
+    fn from(token: NodeData) -> Self {
+        Self::Leaf(token)
+    }
+}
+
+impl Debug for NodeData {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        write!(f, "{:?}: {}", self.kind, self.len)
+    }
+}
+
+impl PartialEq for NodeData {
+    fn eq(&self, other: &Self) -> bool {
+        self.kind == other.kind && self.len == other.len
+    }
+}
diff --git a/src/parse/parser.rs b/src/syntax/parser.rs
similarity index 99%
rename from src/parse/parser.rs
rename to src/syntax/parser.rs
index 3dbb7d50c..83b333f4f 100644
--- a/src/parse/parser.rs
+++ b/src/syntax/parser.rs
@@ -2,8 +2,7 @@ use std::fmt::{self, Display, Formatter};
 use std::mem;
 use std::ops::Range;
 
-use super::{TokenMode, Tokens};
-use crate::syntax::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode};
+use super::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode, TokenMode, Tokens};
 use crate::util::EcoString;
 
 /// A convenient token-based parser.
diff --git a/src/parse/mod.rs b/src/syntax/parsing.rs
similarity index 97%
rename from src/parse/mod.rs
rename to src/syntax/parsing.rs
index ac8ec6eb8..10b4c4c2b 100644
--- a/src/parse/mod.rs
+++ b/src/syntax/parsing.rs
@@ -1,18 +1,10 @@
-//! Parsing and tokenization.
-
-mod incremental;
-mod parser;
-mod resolve;
-mod tokens;
-
-pub use incremental::*;
-pub use parser::*;
-pub use tokens::*;
-
 use std::collections::HashSet;
 
-use crate::syntax::ast::{Assoc, BinOp, UnOp};
-use crate::syntax::{ErrorPos, NodeKind, SyntaxNode};
+use super::ast::{Assoc, BinOp, UnOp};
+use super::{
+    ErrorPos, Group, Marker, NodeKind, ParseError, ParseResult, Parser, SyntaxNode,
+    TokenMode,
+};
 use crate::util::EcoString;
 
 /// Parse a source file.
@@ -32,7 +24,7 @@ pub fn parse_code(text: &str) -> SyntaxNode {
 /// Reparse a code block.
 ///
 /// Returns `Some` if all of the input was consumed.
-fn reparse_code_block(
+pub(crate) fn reparse_code_block(
     prefix: &str,
     text: &str,
     end_pos: usize,
@@ -56,7 +48,7 @@ fn reparse_code_block(
 /// Reparse a content block.
 ///
 /// Returns `Some` if all of the input was consumed.
-fn reparse_content_block(
+pub(crate) fn reparse_content_block(
     prefix: &str,
     text: &str,
     end_pos: usize,
@@ -80,7 +72,7 @@ fn reparse_content_block(
 /// Reparse a sequence markup elements without the topmost node.
 ///
 /// Returns `Some` if all of the input was consumed.
-fn reparse_markup_elements(
+pub(crate) fn reparse_markup_elements(
     prefix: &str,
     text: &str,
     end_pos: usize,
@@ -1146,21 +1138,3 @@ fn body(p: &mut Parser) -> ParseResult {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use std::fmt::Debug;
-
-    #[track_caller]
-    pub fn check(text: &str, found: T, expected: T)
-    where
-        T: Debug + PartialEq,
-    {
-        if found != expected {
-            println!("source:   {text:?}");
-            println!("expected: {expected:#?}");
-            println!("found:    {found:#?}");
-            panic!("test failed");
-        }
-    }
-}
diff --git a/src/parse/resolve.rs b/src/syntax/resolve.rs
similarity index 99%
rename from src/parse/resolve.rs
rename to src/syntax/resolve.rs
index 9fde0cf40..2ad35cecd 100644
--- a/src/parse/resolve.rs
+++ b/src/syntax/resolve.rs
@@ -1,7 +1,6 @@
 use unscanny::Scanner;
 
-use super::{is_ident, is_newline};
-use crate::syntax::RawKind;
+use super::{is_ident, is_newline, RawKind};
 use crate::util::EcoString;
 
 /// Resolve all escape sequences in a string.
diff --git a/src/source.rs b/src/syntax/source.rs
similarity index 99%
rename from src/source.rs
rename to src/syntax/source.rs
index 69e72d6bc..1b87b1c93 100644
--- a/src/source.rs
+++ b/src/syntax/source.rs
@@ -9,8 +9,8 @@ use comemo::Prehashed;
 use unscanny::Scanner;
 
 use crate::diag::SourceResult;
-use crate::parse::{is_newline, parse, reparse};
 use crate::syntax::ast::Markup;
+use crate::syntax::{is_newline, parse, reparse};
 use crate::syntax::{Span, SyntaxNode};
 use crate::util::{PathExt, StrExt};
 
diff --git a/src/syntax/span.rs b/src/syntax/span.rs
index 744aa123f..d4d9a8f61 100644
--- a/src/syntax/span.rs
+++ b/src/syntax/span.rs
@@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Display, Formatter};
 use std::num::NonZeroU64;
 use std::ops::Range;
 
-use crate::syntax::SourceId;
+use super::SourceId;
 
 /// A value with a span locating it in the source code.
 #[derive(Copy, Clone, Eq, PartialEq, Hash)]
@@ -42,8 +42,8 @@ impl Debug for Spanned {
 /// 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](crate::source::Source::range)
-/// to a byte range for user facing display.
+/// or element stems from. Can be [mapped back](super::Source::range) to a byte
+/// range for user facing display.
 ///
 /// Span ids are ordered in the tree to enable quickly finding the node with
 /// some id:
diff --git a/src/parse/tokens.rs b/src/syntax/tokens.rs
similarity index 99%
rename from src/parse/tokens.rs
rename to src/syntax/tokens.rs
index 73c64d1e1..8e1b69444 100644
--- a/src/parse/tokens.rs
+++ b/src/syntax/tokens.rs
@@ -4,8 +4,8 @@ use unicode_xid::UnicodeXID;
 use unscanny::Scanner;
 
 use super::resolve::{resolve_hex, resolve_raw, resolve_string};
+use super::{ErrorPos, NodeKind, RawKind, Unit};
 use crate::geom::{AngleUnit, LengthUnit};
-use crate::syntax::{ErrorPos, NodeKind, RawKind, Unit};
 use crate::util::EcoString;
 
 /// An iterator over the tokens of a string of source code.
@@ -710,8 +710,8 @@ fn is_math_id_continue(c: char) -> bool {
 #[cfg(test)]
 #[allow(non_snake_case)]
 mod tests {
+    use super::super::tests::check;
     use super::*;
-    use crate::parse::tests::check;
 
     use ErrorPos::*;
     use NodeKind::*;
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 9eab55c86..235fcab37 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -15,15 +15,13 @@ use unscanny::Scanner;
 use walkdir::WalkDir;
 
 use typst::diag::{FileError, FileResult};
-use typst::eval::{Smart, Value};
 use typst::font::{Font, FontBook};
 use typst::frame::{Element, Frame};
 use typst::geom::{Length, RgbaColor, Sides};
 use typst::library::layout::PageNode;
 use typst::library::text::{TextNode, TextSize};
-use typst::model::StyleMap;
-use typst::source::{Source, SourceId};
-use typst::syntax::SyntaxNode;
+use typst::model::{Smart, StyleMap, Value};
+use typst::syntax::{Source, SourceId, SyntaxNode};
 use typst::util::{Buffer, PathExt};
 use typst::{bail, Config, World};