use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::num::NonZeroU32;
use std::ops::Deref;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub struct Span {
start: u32,
end: u32,
}
impl Span {
pub const SYNTHETIC: Self = Self {
start: u32::MAX,
end: 0,
};
pub const fn new(start: u32, end: u32) -> Self {
assert!(start <= end, "span start must be <= span end");
Self { start, end }
}
pub const fn start(&self) -> u32 {
self.start
}
pub const fn end(&self) -> u32 {
self.end
}
pub const fn len(&self) -> u32 {
if self.is_synthetic() {
0
} else {
self.end - self.start
}
}
pub const fn contains(&self, offset: u32) -> bool {
!self.is_synthetic() && self.start <= offset && offset < self.end
}
pub const fn is_empty(&self) -> bool {
!self.is_synthetic() && self.start == self.end
}
pub const fn union(&self, other: Self) -> Self {
if self.is_synthetic() {
other
} else if other.is_synthetic() {
*self
} else {
Self {
start: if self.start < other.start {
self.start
} else {
other.start
},
end: if self.end > other.end {
self.end
} else {
other.end
},
}
}
}
pub const fn is_synthetic(&self) -> bool {
self.start == u32::MAX && self.end == 0
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub struct Symbol(NonZeroU32);
impl Symbol {
pub const fn new(raw: u32) -> Option<Self> {
match NonZeroU32::new(raw) {
Some(raw) => Some(Self(raw)),
None => None,
}
}
pub const fn as_u32(&self) -> u32 {
self.0.get()
}
pub const fn index(&self) -> usize {
(self.0.get() - 1) as usize
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FoldedSymbol(NonZeroU32);
impl FoldedSymbol {
pub const fn new(raw: u32) -> Option<Self> {
match NonZeroU32::new(raw) {
Some(raw) => Some(Self(raw)),
None => None,
}
}
pub const fn as_u32(&self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub struct NodeId(NonZeroU32);
impl NodeId {
pub const fn new(raw: u32) -> Option<Self> {
match NonZeroU32::new(raw) {
Some(raw) => Some(Self(raw)),
None => None,
}
}
pub const fn as_u32(&self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
pub struct Meta {
pub span: Span,
pub node_id: NodeId,
}
impl Meta {
pub const fn new(span: Span, node_id: NodeId) -> Self {
Self { span, node_id }
}
}
impl PartialEq for Meta {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl Eq for Meta {}
impl Hash for Meta {
fn hash<H: Hasher>(&self, _state: &mut H) {}
}
impl PartialOrd for Meta {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Meta {
fn cmp(&self, _other: &Self) -> Ordering {
Ordering::Equal
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LineIndex {
newline_offsets: Vec<u32>,
}
impl LineIndex {
#[expect(
clippy::should_implement_trait,
reason = "the ticket requires an inherent LineIndex::from_str constructor"
)]
pub fn from_str(source: &str) -> Self {
let mut newline_offsets = Vec::new();
for (offset, byte) in source.bytes().enumerate() {
if byte == b'\n' {
let offset =
u32::try_from(offset).expect("source byte offset exceeds u32 span range");
newline_offsets.push(offset);
}
}
Self { newline_offsets }
}
pub fn lookup(&self, offset: u32) -> (u32, u32) {
let line = self
.newline_offsets
.partition_point(|&newline| newline < offset);
let line_start = match line {
0 => 0,
_ => self.newline_offsets[line - 1] + 1,
};
(
u32::try_from(line).expect("line count exceeds u32::MAX"),
offset - line_start,
)
}
}
impl std::str::FromStr for LineIndex {
type Err = std::convert::Infallible;
fn from_str(source: &str) -> Result<Self, Self::Err> {
Ok(Self::from_str(source))
}
}
pub trait Resolver {
fn try_resolve(&self, sym: Symbol) -> Option<&str>;
fn resolve(&self, sym: Symbol) -> &str {
self.try_resolve(sym)
.unwrap_or_else(|| panic!("unknown symbol {}", sym.as_u32()))
}
}
pub trait SourceStore: Clone + Deref<Target = str> + 'static {}
impl<T> SourceStore for T where T: Clone + Deref<Target = str> + 'static {}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::mem::size_of;
use std::rc::Rc;
use std::sync::Arc;
#[test]
fn compact_sizes_use_nonzero_niches() {
assert_eq!(size_of::<Span>(), 8);
assert_eq!(size_of::<Symbol>(), 4);
assert_eq!(size_of::<Option<Symbol>>(), 4);
assert_eq!(size_of::<FoldedSymbol>(), 4);
assert_eq!(size_of::<Option<FoldedSymbol>>(), 4);
assert_eq!(size_of::<NodeId>(), 4);
assert_eq!(size_of::<Meta>(), 12);
}
#[test]
fn span_methods_preserve_half_open_invariants() {
let span = Span::new(2, 5);
assert_eq!(span.start(), 2);
assert_eq!(span.end(), 5);
assert_eq!(span.len(), 3);
assert!(!span.is_empty());
assert!(span.contains(2));
assert!(span.contains(4));
assert!(!span.contains(5));
assert!(!span.contains(1));
let empty = Span::new(7, 7);
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
assert!(!empty.contains(7));
}
#[test]
fn span_union_and_synthetic_behave_correctly() {
let left = Span::new(10, 20);
let right = Span::new(3, 12);
assert_eq!(left.union(right), Span::new(3, 20));
assert_eq!(Span::SYNTHETIC.len(), 0);
assert!(!Span::SYNTHETIC.is_empty());
assert!(!Span::SYNTHETIC.contains(u32::MAX));
assert!(Span::SYNTHETIC.is_synthetic());
assert_eq!(Span::SYNTHETIC.union(left), left);
assert_eq!(left.union(Span::SYNTHETIC), left);
assert_eq!(Span::SYNTHETIC.union(Span::SYNTHETIC), Span::SYNTHETIC);
}
#[test]
fn symbols_are_one_based_keys_with_zero_based_indexes() {
assert!(Symbol::new(0).is_none());
let symbol = Symbol::new(3).expect("non-zero symbol");
assert_eq!(symbol.as_u32(), 3);
assert_eq!(symbol.index(), 2);
}
#[test]
fn folded_symbols_are_one_based_keys() {
assert!(FoldedSymbol::new(0).is_none());
let folded = FoldedSymbol::new(3).expect("non-zero folded symbol");
assert_eq!(folded.as_u32(), 3);
}
#[test]
fn node_ids_are_nonzero_counter_values() {
assert!(NodeId::new(0).is_none());
let node_id = NodeId::new(9).expect("non-zero node id");
assert_eq!(node_id.as_u32(), 9);
}
#[test]
fn meta_makes_derived_node_equality_structural() {
#[derive(Debug, PartialEq, Eq, Hash)]
struct TestNode {
semantic: u8,
meta: Meta,
}
let first = TestNode {
semantic: 1,
meta: meta(0, 5, 1),
};
let same_structure = TestNode {
semantic: 1,
meta: meta(100, 120, 2),
};
let different_structure = TestNode {
semantic: 2,
meta: meta(0, 5, 1),
};
assert_eq!(first, same_structure);
assert_ne!(first, different_structure);
assert_eq!(
first.meta.cmp(&same_structure.meta),
std::cmp::Ordering::Equal
);
assert_eq!(
first.meta.partial_cmp(&same_structure.meta),
Some(std::cmp::Ordering::Equal)
);
let mut map = HashMap::new();
map.insert(first, "hit");
assert_eq!(map.get(&same_structure), Some(&"hit"));
assert_eq!(map.get(&different_structure), None);
}
#[test]
fn line_index_maps_offsets_to_zero_based_byte_columns() {
let index = LineIndex::from_str("ab\ncde\nz");
assert_eq!(index.lookup(0), (0, 0));
assert_eq!(index.lookup(1), (0, 1));
assert_eq!(index.lookup(2), (0, 2));
assert_eq!(index.lookup(3), (1, 0));
assert_eq!(index.lookup(4), (1, 1));
assert_eq!(index.lookup(7), (2, 0));
assert_eq!(index.lookup(8), (2, 1));
}
#[test]
fn line_index_handles_empty_input() {
let index = LineIndex::from_str("");
assert_eq!(index.lookup(0), (0, 0));
}
#[test]
fn resolver_returns_text_for_known_symbols() {
struct TestResolver(&'static [&'static str]);
impl Resolver for TestResolver {
fn try_resolve(&self, sym: Symbol) -> Option<&str> {
self.0.get(sym.index()).copied()
}
}
let resolver = TestResolver(&["users", "id"]);
let users = Symbol::new(1).expect("symbol");
let missing = Symbol::new(3).expect("symbol");
assert_eq!(resolver.try_resolve(users), Some("users"));
assert_eq!(resolver.resolve(users), "users");
assert_eq!(resolver.try_resolve(missing), None);
}
#[test]
fn source_store_admits_owned_and_static_stores() {
fn as_str<S: SourceStore>(source: S) -> String {
source.deref().to_owned()
}
let arc: Arc<str> = Arc::from("select");
let rc: Rc<str> = Rc::from("update");
let owned = String::from("from");
let boxed: Box<str> = Box::from("delete");
let static_borrow: &'static str = "where";
assert_eq!(as_str(arc), "select");
assert_eq!(as_str(rc), "update");
assert_eq!(as_str(owned), "from");
assert_eq!(as_str(boxed), "delete");
assert_eq!(as_str(static_borrow), "where");
}
#[test]
fn source_store_is_always_static() {
fn assert_static_store<S: SourceStore>() {
fn needs_static<T: 'static>() {}
needs_static::<S>();
}
assert_static_store::<Arc<str>>();
assert_static_store::<Rc<str>>();
assert_static_store::<String>();
}
fn meta(start: u32, end: u32, id: u32) -> Meta {
Meta::new(
Span::new(start, end),
NodeId::new(id).expect("non-zero node id"),
)
}
}