// Source spans // // Copyright (C) 2014-2023 Ryan Specialty, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Mapping to source input byte intervals. //! //! A [`Span`] is a mapping to a byte interval within a source file, //! representing primarily where some IR entity originated. //! This underpins the diagnostic system, //! intended to: //! //! 1. Give the user specific information for debugging errors in their //! programs; and //! 2. Provide high-resolution information for source code inquiries, //! such as "where is this identifier?" and "what exists at my cursor //! position within this file"? //! //! A span contains a [`Context`] representing the source location. //! A context's path is a [`PathSymbolId`], //! which represents an interned string slice, //! _not_ a [`PathBuf`](std::path::PathBuf) or //! [`OsStr`](std::ffi::OsStr). //! //! ``` //! use tamer::span::{Span, Context}; //! use tamer::sym::GlobalSymbolIntern; //! //! // From raw parts //! let ctx: Context = "some/path/foo".intern().into(); //! let span = Span::new(2, 6, ctx); //! //! assert_eq!(2, span.offset()); //! assert_eq!(6, span.len()); //! assert_eq!(ctx, span.context()); //! //! // From a closed byte interval //! let spani = Span::from_byte_interval((10, 25), "some/path/bar".intern()); //! assert_eq!(10, spani.offset()); //! assert_eq!(15, spani.len()); //! //! // Freely copyable //! let cp = span; //! assert_eq!(cp, span); //! ``` //! //! Span is expected to be able to fit within a general-purpose CPU register //! on a 64-bit system, and so does not exceed 8 bytes in length. //! //! Spans are one of the most common objects in TAMER, //! competing only with [symbols](crate::sym). //! But unlike symbols, //! [`Span`]s are designed to be meaningfully identifiable and copyable //! without interning. //! //! A span is ordered as such: //! //! 1. Spans group by [`Context`], //! though the relative ordering of each [`Context`] isn't //! necessarily meaningful; //! 2. Spans are then ordered relative to their offset; and //! 3. Spans are finally ordered by their length. //! //! Note that this means that a span beginning after but ending before //! another span will still order higher, //! as shown in the example below. //! //! ``` //! # use tamer::span::{Span, Context}; //! # use tamer::sym::GlobalSymbolIntern; //! # //! # let ctx: Context = "some/path/foo".intern().into(); //! # //! // Visualization of spans: //! // [....,....,....,....,] //! // [A-+-] [B-+]| //! // | [C-] | //! // | [D-+-] //! // | [E] //! // [F----] | //! // [G------] //! //! let A = Span::new(2, 6, ctx); //! let B = Span::new(10, 5, ctx); //! let C = Span::new(10, 4, ctx); //! let D = Span::new(10, 6, ctx); //! let E = Span::new(11, 3, ctx); //! let F = Span::new(5, 7, ctx); //! let G = Span::new(5, 8, ctx); //! //! let mut spans = vec![A, B, C, D, E, F, G]; //! spans.sort(); //! //! assert_eq!(spans, vec![A, F, G, C, B, D, E]); //! ``` //! //! Design Rationale //! ================ //! It is expected that spans will be created and copied frequently, //! as they are propagated to every IR in the system. //! It is further expected that the data within a span will only be //! referenced for diagnostic purposes, //! or for utilities operating on original source code //! (such as code formatters). //! //! When a span is referenced, //! it will either be to determine the exact location of a particular //! entity, //! or it will be to attempt to locate a similar entity in a higher-level //! IR associated with the same region of code. //! The latter requires that spans be comparable in a meaningful way, //! exhibiting at least partial ordering. //! //! Spans are therefore optimized for three primary use cases: //! - copying; //! - comparison; and //! - ordering. //! //! Span Structure //! -------------- //! Spans are packed into 64-bit values that can be readily converted into a //! [`u64`] value that is totally ordered relative to a given [`Context`], //! byte offset, and byte length. //! This means that sorting a collection of [`Span`]s will group spans by //! their [`Context`]; //! will sort those spans relative to their starting offset within that //! context; and //! will sort again by the ending offset. //! //! This means that spans are [`Eq`] and [`Ord`], //! and efficiently so by simply comparing the byte values of the entire //! struct as a single [`u64`]. //! This allows spans to be sorted relative to their positions within a //! context; //! be placed into a binary tree for mapping back to higher-level IRs; //! gives spans a meaningful unique identifier; //! be freely copied without cost; //! and more, //! all very efficiently and without having to access individual //! struct members. //! //! To accomplish this, //! [`Span`] uses `repr(packed)` and orders the fields for little endian //! systems like `x86_64`, //! which is what our team uses. //! The `packed` representation had to be used because the byte orderings //! are [`u16`], [`u32`], [`u16`], //! which makes the [`u32`] byte offset unaligned. //! Note that, //! while this _is_ unaligned, //! this is _not_ unaligned _memory_ access, //! since the entire [`Span`] will be retrieved from (aligned) memory at //! once; //! the unaligned fields within the [`u64`] do not incur a measurable //! performance cost. //! //! Related Work //! ============ //! This span is motivated by [rustc's compressed `Span`](rustc-span). //! TAMER's span size relies on 16 bits being sufficient for holding //! interned paths, //! which _should_ be a very reasonable assumption unless the interner //! ends up being shared with too many different things. //! If ever that assumption becomes violated, //! and it is deemed that packages containing so many symbols should be permitted, //! TAMER's [`Span`] can accommodate in a similar with to rustc's by //! interning the larger span data and tagging this span as such. //! //! //! [rustc-span]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_span/struct.Span.html use crate::{ debug_diagnostic_panic, global, sym::{st16, ContextStaticSymbolId, GlobalSymbolResolve, SymbolId}, }; use std::{convert::TryInto, fmt::Display, path::Path}; /// A symbol size sufficient for holding interned paths. pub type PathSymbolId = SymbolId; /// Size of a [`Span`]'s `offset` field. pub type SpanOffsetSize = global::SourceFileSize; /// Size of a [`Span`]'s `len` field. pub type SpanLenSize = global::FrontendTokenLength; /// Description of a source location and byte interval for some object. /// /// Spans represent byte intervals within a given source context. /// A span should map to useful positions for helping users debug error /// messages. /// If code is generated, desugared, or otherwise manipulated, /// the span ought to reference the original location of the code that can /// be referenced and modified to correct any problems. /// /// See the [module-level documentation](self) for more information. #[cfg(target_endian = "little")] #[repr(packed)] #[derive(Debug, Clone, Copy)] pub struct Span { /// Token length (ending byte offset - `offset`). len: SpanLenSize, /// Starting 0-indexed byte position, inclusive. offset: SpanOffsetSize, /// Context onto which byte offsets are mapped, /// such as a source file. /// /// N.B.: This is an unaligned field, /// and accessing it frequently may have a negative impact on /// performance. ctx: Context, } assert_eq_size!(Span, Option); impl Span { /// Create a new span from its constituent parts. pub fn new>( offset: SpanOffsetSize, len: SpanLenSize, ctx: C, ) -> Self { Self { ctx: ctx.into(), offset, len, } } /// Create a constant span from a static context. pub const fn st_ctx(sym: ContextStaticSymbolId) -> Self { Self { ctx: Context(sym.as_sym()), offset: 0, len: 0, } } /// Create a span from a byte interval and context. /// /// Panics /// ====== /// This will panic in the unlikely case that the difference between the /// start and end of the interval exceeds the maximum of /// [`global::FrontendTokenLength`]. /// /// If this error occurs, /// the parser should consider splitting large tokens up into multiple /// tokens; /// increasing [`global::FrontendTokenLength`] should be a last /// resort, /// since it has wide-reaching implications on the size of /// [`Span`]. /// /// The user is not expected to know how to recover from this error /// without debugging the compiler. /// It is not expected that this would occur on any valid inputs. pub fn from_byte_interval(interval: B, ctx: C) -> Self where B: Into, C: Into, { let binterval = interval.into(); Self { offset: binterval.0, len: (binterval.1 - binterval.0) .try_into() .expect("span length exceeds global::FrontendTokenLength"), ctx: ctx.into(), } } // A span represented uniquely as a totally ordered [`u64`]. // // For more information on this important properly, // see the documentation for [`Span`] itself. pub fn as_u64(self) -> u64 { // We take a number of precautions to make this safe (in the sense // of correctness), // through struct packing and a `cfg` directive for endianness. // In any case, // a `u64` isn't going to harm anyone. unsafe { std::mem::transmute(self) } } /// Byte offset of the beginning of the span relative to its context. pub fn offset(&self) -> SpanOffsetSize { self.offset } /// Length of the span in bytes. /// /// The interval of the span is `[offset, offset+len]`. pub fn len(&self) -> SpanLenSize { self.len } /// The context to which the span applies. /// /// The context is, for example, a file. pub fn context(&self) -> Context { self.ctx } /// Further offset a span. /// /// This attempts to offset a span relative to its current offset by the /// provided value. /// If the resulting offset exceeds [`SpanOffsetSize`], /// the result will be [`None`]. pub const fn offset_add(self, value: SpanOffsetSize) -> Option { match self.offset.checked_add(value) { Some(offset) => Some(Self { offset, ..self }), None => None, } } /// Create two zero-length spans representing respectively the first and /// last offsets in the span. /// /// The second endpoint will be [`None`] if the offset cannot be /// represented by [`SpanOffsetSize`]. /// /// ``` /// # use tamer::span::{Span, Context}; /// # use tamer::sym::GlobalSymbolIntern; /// # /// # let ctx: Context = "some/path/foo".intern().into(); /// # /// // [0123456789] /// // [---] /// // 2 6 /// // A /// let A = Span::new(2, 6, ctx); /// /// assert_eq!( /// A.endpoints(), /// ( /// Span::new(2, 0, ctx), /// Some(Span::new(8, 0, ctx)), /// ), /// ); /// ``` pub const fn endpoints(self) -> (Self, Option) { ( // First endpoint. Self { offset: self.offset, len: 0, ..self }, // Second endpoint. match self.offset.checked_add(self.len as u32) { Some(offset) => Some(Self { offset, len: 0, ..self }), None => None, }, ) } /// Create two zero-length spans representing respectively the first and /// last offsets in the span, /// saturating the ending offset if it cannot be represented by /// [`SpanOffsetSize`]. /// /// Aside from the saturation, /// this is identical to [`Span::endpoints`]. pub fn endpoints_saturated(self) -> (Self, Self) { let endpoints = self.endpoints(); ( endpoints.0, endpoints.1.unwrap_or(Self { offset: SpanOffsetSize::MAX, ..endpoints.0 }), ) } /// Create a new span that is a slice of this one. /// /// If either `rel_offset` or `len` are too large, /// then a copy of the span will be returned unsliced. /// /// Panics (Debug Mode) /// ------------------- /// If the offset and length exceeds the bounds of the span, /// then the system has an arithmetic bug that ought to be corrected, /// and so this will panic with a diagnostic message. /// This check does not occur on release builds since this is not a /// safety issue and should be caught by tests. pub fn slice(self, rel_offset: usize, len: usize) -> Self { let (irel_offset, ilen) = match (rel_offset.try_into(), len.try_into()) { (Ok(x), Ok(y)) => (x, y), _ => (0, self.len()), }; // We shouldn't ignore slices that exceed the length of the span, // since this represents a bug that'll cause nonsense diagnostic // data and it represents an arithmetic bug in the system // (but there are no safety concerns). if ((irel_offset as usize).saturating_add(ilen as usize)) > self.len() as usize { #[cfg(debug_assertions)] use crate::diagnose::Annotate; debug_diagnostic_panic!( self.error("attempting to slice this span").into(), "length {len} at offset {rel_offset} \ exceeds bounds of span {self}", ); } Self { ctx: self.ctx, offset: self.offset.saturating_add(irel_offset), len: ilen, } } /// Adjust span such that its offset is relative to the provided span. /// /// If the provide `rel_span` does not precede this span, /// [`None`] will be returned. /// /// If the two spans do not share the same [`Context`], /// no comparison can be made and [`None`] will be returned. pub fn relative_to(self, rel_span: Span) -> Option { // Note that this is unaligned. if self.context() != rel_span.context() { return None; } if self.offset() < rel_span.offset() { return None; } Some(Self { ctx: self.ctx, offset: self.offset.saturating_sub(rel_span.offset), len: self.len, }) } /// Merge with another span `b` such that the combined span begins at /// the offset of the earlier of the two spans and extends to the end /// of the later of the two. /// /// Both spans must have the same [`Context`], /// otherwise the result will be [`None`]. /// Merged values beyond [`SpanOffsetSize`] and [`SpanLenSize`] will /// also result in [`None`]. /// /// This properly handles overlapping spans, /// including the case where one of the spans is entirely contained /// within another. /// See test cases for more information. /// (TODO: Maybe we should move the test cases into these docs?) pub fn merge>(self, other: S) -> Option { let b = other.into(); if self.context() != b.context() { return None; } // Order arguments such that `self` is placed at or before `b` // rather than having to worry about confounding accommodations // below. if self.offset() > b.offset() { return b.merge(self); } let (_, end) = b.endpoints(); end.and_then(|Span { offset, .. }| { SpanLenSize::try_from(offset - self.offset).ok() }) .map(|new_len| Self { ctx: self.ctx, offset: self.offset, len: self.len.max(new_len), }) } } impl From for u64 { fn from(val: Span) -> Self { val.as_u64() } } impl PartialEq for Span { fn eq(&self, other: &Self) -> bool { self.as_u64() == other.as_u64() } } impl Eq for Span {} impl PartialOrd for Span { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Span { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_u64().cmp(&other.as_u64()) } } // This assertion verifies our above expectations. // If this fails, // then you have either modified [`global`] constants or you have modified // the fields of [`Span`] itself, // in which case you should read "Related Work" above to determine // whether this was a good idea or if interned spans should be // introduced. // In any case, // hopefully this was planned for, // because otherwise your week has just been ruined. assert_eq_size!(Span, u64); impl From for (Span, Span) { /// Expand a [`Span`] into a two-span. /// /// A two-span `(A, B)` is equivalent to a span beginning at the start /// of `A` and ending at the end of `B`. /// /// We gain no resolution from performing this operation, /// but it does allow for using a single span in contexts where a /// higher resolution is supported. fn from(span: Span) -> Self { (span, span) } } impl Display for Span { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Needed to avoid unaligned references since Span is packed. let ctx = self.ctx; let offset = self.offset as usize; let end = offset + self.len as usize; // Very primitive information to begin with; we'll have something // more useful in the future. write!(f, "[{} offset {}-{}]", ctx, offset, end) } } /// A placeholder span indicating that a span is expected but is not yet /// known. pub const UNKNOWN_SPAN: Span = Span::st_ctx(st16::CTX_UNKNOWN); /// Context for byte offsets (e.g. a source file). /// /// A context is lifetime-free and [`Copy`]-able, /// with the assumption that an interned [`PathSymbolId`] will only need /// to be resolved to its underlying value in a diagnostic context where /// the internment system is readily available. /// /// Since this is used within [`Span`], /// it must be kept as small as possible. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub struct Context(PathSymbolId); impl Context { /// Produce a [`Span`] within the given context. #[inline] pub const fn span(self, offset: SpanOffsetSize, len: SpanLenSize) -> Span { Span { ctx: self, offset, len, } } /// Attempt to produce a [`Span`] of the given length at the given /// offset, /// otherwise fall back to a `(0,0)` (ZZ) span. /// /// If the offset cannot be stored, /// then the length will always be `0` even if it could otherwise be /// represented; /// `(0,0)` indicates no span, /// whereas `(0,N)` would indicate a span of length `N` at /// offset `0`, /// which would not be true. /// /// If the offset can be represented but not the length, /// then a zero-length span at that offset will be produced, /// which still provides useful information. /// This may be the case for very large objects, /// like compiled text fragments. /// /// The rationale here is that spans are intended to be informative. /// If we are unable to provide that information due to exceptional /// circumstances /// (very large file or very large token), /// then it's better to provide _some_ information than to bail out /// with an error and interrupt the entire process, /// potentially masking errors in the process. #[inline] pub fn span_or_zz(self, offset: usize, len: usize) -> Span { self.span(offset.try_into().unwrap_or(0), len.try_into().unwrap_or(0)) } } /// A placeholder context indicating that a context is expected but is not /// yet known. pub const UNKNOWN_CONTEXT: Context = Context(st16::raw::CTX_UNKNOWN); impl> From

for Context { fn from(sym: P) -> Self { Self(sym.into()) } } impl Display for Context { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl AsRef for Context { fn as_ref(&self) -> &Path { Path::new(self.0.lookup_str()) } } /// A closed interval (range of values including its endpoints) representing /// source bytes associated with a token. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct ClosedByteInterval(pub T, pub T) where T: Copy + PartialOrd; impl From<(T, T)> for ClosedByteInterval { /// Convert a tuple into a closed byte interval where the first index /// represents the start of the interval and the second index the /// end. /// /// Panics /// ====== /// The second value must be ≥ the first. fn from(src: (T, T)) -> Self { assert!(src.1 >= src.0); Self(src.0, src.1) } } assert_eq_size!(ClosedByteInterval, u64); /// Dummy spans for testing. #[cfg(test)] pub mod dummy { use super::{st16, Context, Span}; /// A dummy span that can be used in contexts where a span is expected /// but is not important. /// /// This is intended primarily for tests; /// you should always use an appropriate span to permit sensible error /// messages and source analysis. /// For spans that are actually unknown, /// use [`super::UNKNOWN_SPAN`]. /// /// Additional dummy spans can be derived from this one. pub const DUMMY_SPAN: Span = Span::st_ctx(st16::CTX_DUMMY); /// A dummy context that can be used where a span is expected but is not /// important. /// /// This is intended primarily for tests; /// you should always use an appropriate span to permit sensible error /// messages and source analysis. /// For contexts that are actually unknown, /// use [`super::UNKNOWN_CONTEXT`]. /// /// See also [`UNKNOWN_CONTEXT`]. pub const DUMMY_CONTEXT: Context = Context(st16::raw::CTX_DUMMY); // This name is for brevity; // we don't want to expose it because we don't want anyone to assume // that a different name means that it's somehow different from // `DUMMY_SPAN`. const S0: Span = DUMMY_SPAN; pub const S1: Span = S0.offset_add(1).unwrap(); pub const S2: Span = S0.offset_add(2).unwrap(); pub const S3: Span = S0.offset_add(3).unwrap(); pub const S4: Span = S0.offset_add(4).unwrap(); pub const S5: Span = S0.offset_add(5).unwrap(); pub const S6: Span = S0.offset_add(6).unwrap(); pub const S7: Span = S0.offset_add(7).unwrap(); pub const S8: Span = S0.offset_add(8).unwrap(); pub const S9: Span = S0.offset_add(9).unwrap(); pub const S10: Span = S0.offset_add(10).unwrap(); pub const S11: Span = S0.offset_add(11).unwrap(); pub const S12: Span = S0.offset_add(12).unwrap(); pub const S13: Span = S0.offset_add(13).unwrap(); pub const S14: Span = S0.offset_add(14).unwrap(); pub const S15: Span = S0.offset_add(15).unwrap(); pub const S16: Span = S0.offset_add(16).unwrap(); pub const S17: Span = S0.offset_add(17).unwrap(); pub const S18: Span = S0.offset_add(18).unwrap(); pub const S19: Span = S0.offset_add(19).unwrap(); pub const S20: Span = S0.offset_add(20).unwrap(); } #[cfg(test)] mod test { use super::*; // Little endian check. // // This ensures that the byte ordering is as expected, // otherwise the resulting integer will not have the properties we // require for sorting and comparison. #[cfg(target_endian = "little")] #[test] fn span_pack_le() { let span = Span::new(0xA3A2A1A0, 0xB1B0, SymbolId::test_from_int(0xC1C0)); assert_eq!( 0xC1C0_A3A2A1A0_B1B0, // ^ ^ ^ // ctx offset len span.as_u64(), "endianness check failed: {:X?}", span.as_u64() ); } #[cfg(target_endian = "big")] #[test] fn span_pack_be_not_supported() { panic!("Big-endian systems are not currently supported"); } // The tests that follow are corollaries of the above, but the below // tests do test that the implementations function as intended. #[test] fn span_at_later_offset_in_same_context_compares_greater() { let ctx = Context::from("imaginary"); let first = ctx.span(10, 5); let second = ctx.span(20, 5); // These two assertions must be identical. assert!(second > first); assert!(second.as_u64() > first.as_u64()); } #[test] fn spans_order_by_context_start_and_len() { let ctxa = Context::from("context a"); let ctxb = Context::from("context b"); // Sanity check, otherwise this test won't work as expected. assert!(ctxa.0 < ctxb.0); let sa1 = ctxa.span(10, 1); let sa2 = ctxa.span(22, 1); let sa3 = ctxa.span(35, 1); let sb1 = ctxb.span(11, 1); let sb2 = ctxb.span(20, 1); let sb3 = ctxb.span(33, 1); let mut spans = vec![sa3, sb2, sb1, sa2, sa1, sb3]; spans.sort(); assert_eq!(spans, vec![sa1, sa2, sa3, sb1, sb2, sb3]); } #[test] fn retrieve_span_components() { let ctx = Context::from("foo"); let offset = 100; let len = 50; let span = ctx.span(offset, len); assert_eq!( (offset, len, ctx), (span.offset(), span.len(), span.context()) ); } #[test] fn span_offset_add() { let ctx = Context::from("addtest"); let offset = 10; let len = 5; let span = ctx.span(offset, len); // Successful add. assert_eq!( span.offset_add(10), Some(Span { offset: offset + 10, len, ctx }) ); // Fail, do not wrap. assert_eq!(span.offset_add(SpanOffsetSize::MAX), None); } #[test] fn span_into_twospan() { let ctx = Context::from("foo"); let span = ctx.span(10, 50); assert_eq!((span, span), span.into()); } #[test] fn span_endpoints() { let ctx = Context::from("end"); let span = ctx.span(10, 20); let (start, end) = span.endpoints(); assert_eq!(start, Span::new(10, 0, ctx)); assert_eq!(end, Some(Span::new(30, 0, ctx))); } #[test] fn span_endpoints_exceeding_max_offset() { let ctx = Context::from("end"); let offset = SpanOffsetSize::MAX - 5; let span = ctx.span(offset, 10); let (start, end) = span.endpoints(); assert_eq!(start, Span::new(offset, 0, ctx)); assert_eq!(end, None); } #[test] fn span_endpoints_saturated() { let ctx = Context::from("end"); let offset = SpanOffsetSize::MAX - 5; let span = ctx.span(offset, 10); let (start, end) = span.endpoints_saturated(); assert_eq!(start, Span::new(offset, 0, ctx)); assert_eq!(end, Span::new(SpanOffsetSize::MAX, 0, ctx)); } #[test] fn span_slice_yields_slice_within_original() { let ctx = Context::from("slice"); let span = ctx.span(10, 10); assert_eq!(ctx.span(15, 5), span.slice(5, 5)); } #[test] fn span_slice_large_values_yield_original() { let span = Context::from("slice").span(0, 50); // Too large of an offset should return original even though legnth // is okay. assert_eq!(span, span.slice(usize::MAX, 5)); // Too large of length should return original even though offset is // okay. assert_eq!(span, span.slice(0, usize::MAX)); } #[test] fn span_merge_one_after_other() { let ctx = Context::from("merge"); // "an example string" // [-----] [----] // 3 9 11 16 // | A B | // [------------] // 3 16 // C let a = ctx.span(3, 7); let b = ctx.span(11, 6); let c = ctx.span(3, 14); assert_eq!(a.merge(b), Some(c)); assert_eq!(b.merge(a), Some(c)); } #[test] fn span_merge_overlap() { let ctx = Context::from("merge"); // "an example string" // [---+-] | // 3 | 9 | // | A| | // | [--------] // | 7 16 // | B | // [------------] // 3 16 // C let a = ctx.span(3, 7); let b = ctx.span(7, 10); let c = ctx.span(3, 14); // We compare in both orders, // so this will test when a span overlaps on either side. assert_eq!(a.merge(b), Some(c)); assert_eq!(b.merge(a), Some(c)); } #[test] fn span_merge_overlap_within() { let ctx = Context::from("merge"); // "an example string" // |[----] | // |1 6 | // | B | // [--------] // 0 9 // C let b = ctx.span(1, 6); let c = ctx.span(0, 10); assert_eq!(b.merge(c), Some(c)); assert_eq!(c.merge(b), Some(c)); } // It doesn't make sense to merge two spans that are located in // different contexts. #[test] fn span_merge_different_contexts() { let ctx_a = Context::from("merge_a"); let ctx_b = Context::from("merge_b"); assert_eq!(ctx_a.span(0, 1).merge(ctx_b.span(1, 2)), None); } }