//! The `.gdc` s-expression reader: hand-written, span-carrying, and //! comment-recording, with no dependencies. //! //! Grammar: lists `( ... )`, symbols (`container`, `flex-start`, `+`, `i32`), //! keywords (`:class`), double-quoted strings with `\\ \" \n \t \{` //! escapes, integers, and floats. Line comments start with `;`. //! //! Every value carries its byte span in the source. Comments are collected //! into a side table with spans, which together with the value spans is //! sufficient for a future formatter to reproduce a file losslessly. use std::fmt; /// A byte range in the source text. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Span { pub start: usize, pub end: usize, } impl Span { pub fn new(start: usize, end: usize) -> Self { Self { start, end } } /// A span covering both. pub fn to(self, other: Span) -> Span { Span::new(self.start.min(other.start), self.end.max(other.end)) } } /// One s-expression value. #[derive(Clone, Debug, PartialEq)] pub enum Sexpr { List(Vec, Span), /// A bare symbol: `container`, `flex-start`, `i32`, `+`. Symbol(String, Span), /// A keyword: `:class` (stored without the colon). Keyword(String, Span), /// A string literal, with escapes already processed. Str(String, Span), Int(i64, Span), Float(f64, Span), } impl Sexpr { pub fn span(&self) -> Span { match self { Sexpr::List(_, s) | Sexpr::Symbol(_, s) | Sexpr::Keyword(_, s) | Sexpr::Str(_, s) | Sexpr::Int(_, s) | Sexpr::Float(_, s) => *s, } } pub fn as_symbol(&self) -> Option<&str> { match self { Sexpr::Symbol(s, _) => Some(s), _ => None, } } pub fn as_list(&self) -> Option<&[Sexpr]> { match self { Sexpr::List(items, _) => Some(items), _ => None, } } /// A short human name for the value's kind, for diagnostics. pub fn kind_name(&self) -> &'static str { match self { Sexpr::List(..) => "list", Sexpr::Symbol(..) => "symbol", Sexpr::Keyword(..) => "keyword", Sexpr::Str(..) => "string", Sexpr::Int(..) => "integer", Sexpr::Float(..) => "float", } } } /// A comment, with its span (including the `;`). #[derive(Clone, Debug, PartialEq)] pub struct Comment { pub text: String, pub span: Span, } /// The result of reading a source file: top-level values plus the comment /// side table. #[derive(Debug, Default)] pub struct Document { pub values: Vec, pub comments: Vec, } /// A read error with location. #[derive(Debug, Clone, PartialEq)] pub struct ReadError { pub message: String, pub span: Span, } impl fmt::Display for ReadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.message) } } /// Read a whole source text into a [`Document`]. pub fn read(source: &str) -> Result { let mut reader = Reader { source, bytes: source.as_bytes(), pos: 0, comments: Vec::new(), }; let mut values = Vec::new(); loop { reader.skip_trivia(); if reader.at_end() { break; } values.push(reader.value()?); } Ok(Document { values, comments: reader.comments, }) } struct Reader<'a> { source: &'a str, bytes: &'a [u8], pos: usize, comments: Vec, } impl Reader<'_> { fn at_end(&self) -> bool { self.pos >= self.bytes.len() } fn peek(&self) -> Option { self.bytes.get(self.pos).copied() } fn error(&self, message: impl Into, span: Span) -> ReadError { ReadError { message: message.into(), span, } } /// Skip whitespace and comments, recording the comments. fn skip_trivia(&mut self) { loop { match self.peek() { Some(b) if b.is_ascii_whitespace() => self.pos += 1, Some(b';') => { let start = self.pos; while let Some(b) = self.peek() { if b == b'\n' { break; } self.pos += 1; } self.comments.push(Comment { text: self.source[start..self.pos].to_owned(), span: Span::new(start, self.pos), }); } _ => return, } } } fn value(&mut self) -> Result { self.skip_trivia(); let start = self.pos; match self.peek() { None => Err(self.error("unexpected end of input", Span::new(start, start))), Some(b'(') => self.list(), Some(b')') => { Err(self.error("unmatched closing parenthesis", Span::new(start, start + 1))) } Some(b'"') => self.string(), Some(b':') => self.keyword(), Some(b) if b.is_ascii_digit() => self.number(), Some(b'-') if self .bytes .get(self.pos + 1) .is_some_and(|b| b.is_ascii_digit()) => { self.number() } Some(_) => self.symbol(), } } fn list(&mut self) -> Result { let start = self.pos; self.pos += 1; // consume '(' let mut items = Vec::new(); loop { self.skip_trivia(); match self.peek() { None => { return Err(self.error( "unclosed parenthesis; expected `)`", Span::new(start, start + 1), )); } Some(b')') => { self.pos += 1; return Ok(Sexpr::List(items, Span::new(start, self.pos))); } Some(_) => items.push(self.value()?), } } } fn string(&mut self) -> Result { let start = self.pos; self.pos += 1; // consume '"' let mut text = String::new(); loop { match self.peek() { None => { return Err(self.error("unterminated string", Span::new(start, self.pos))); } Some(b'"') => { self.pos += 1; return Ok(Sexpr::Str(text, Span::new(start, self.pos))); } Some(b'\\') => { let escape_start = self.pos; self.pos += 1; let escaped = match self.peek() { Some(b'n') => '\n', Some(b't') => '\t', Some(b'\\') => '\\', Some(b'"') => '"', // `\{` produces a literal brace, exempt from // interpolation. Some(b'{') => '{', other => { return Err(self.error( match other { Some(c) => { format!("unknown escape `\\{}`", c as char) } None => "unterminated escape".to_owned(), }, Span::new(escape_start, self.pos + 1), )); } }; text.push(escaped); self.pos += 1; } Some(_) => { // Consume one full UTF-8 character. let ch = self.source[self.pos..] .chars() .next() .expect("peek saw a byte"); text.push(ch); self.pos += ch.len_utf8(); } } } } fn keyword(&mut self) -> Result { let start = self.pos; self.pos += 1; // consume ':' let name_start = self.pos; self.consume_symbol_bytes(); if self.pos == name_start { return Err(self.error("`:` must be followed by a name", Span::new(start, self.pos))); } Ok(Sexpr::Keyword( self.source[name_start..self.pos].to_owned(), Span::new(start, self.pos), )) } fn number(&mut self) -> Result { let start = self.pos; self.consume_symbol_bytes(); let text = &self.source[start..self.pos]; let span = Span::new(start, self.pos); if text.contains('.') || text.contains('e') || text.contains('E') { text.parse::() .map(|v| Sexpr::Float(v, span)) .map_err(|_| self.error(format!("invalid number `{text}`"), span)) } else { text.parse::() .map(|v| Sexpr::Int(v, span)) .map_err(|_| self.error(format!("invalid number `{text}`"), span)) } } fn symbol(&mut self) -> Result { let start = self.pos; self.consume_symbol_bytes(); if self.pos == start { let ch = self.source[self.pos..].chars().next().unwrap_or('\0'); return Err(self.error( format!("unexpected character `{ch}`"), Span::new(start, start + ch.len_utf8().max(1)), )); } Ok(Sexpr::Symbol( self.source[start..self.pos].to_owned(), Span::new(start, self.pos), )) } fn consume_symbol_bytes(&mut self) { while let Some(b) = self.peek() { let terminator = b.is_ascii_whitespace() || matches!(b, b'(' | b')' | b'"' | b';' | b':'); if terminator { break; } self.pos += 1; } } } #[cfg(test)] mod tests;