use crate::scheme::value::{Value, Unit};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Integer(i64),
Real(f64),
Quantity(f64, String), String(String),
Char(char),
Symbol(String),
Keyword(String),
Bool(bool),
LeftParen, RightParen, LeftBracket, RightBracket, Dot,
Quote, Quasiquote, Unquote, UnquoteSplicing,
VectorStart,
Eof,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Integer(n) => write!(f, "{}", n),
Token::Real(n) => write!(f, "{}", n),
Token::Quantity(magnitude, unit) => write!(f, "{}{}", magnitude, unit),
Token::String(s) => write!(f, "\"{}\"", s),
Token::Char(ch) => write!(f, "#\\{}", ch),
Token::Symbol(s) => write!(f, "{}", s),
Token::Keyword(s) => write!(f, "#:{}", s),
Token::Bool(b) => write!(f, "{}", if *b { "#t" } else { "#f" }),
Token::LeftParen => write!(f, "("),
Token::RightParen => write!(f, ")"),
Token::LeftBracket => write!(f, "["),
Token::RightBracket => write!(f, "]"),
Token::Dot => write!(f, "."),
Token::Quote => write!(f, "'"),
Token::Quasiquote => write!(f, "`"),
Token::Unquote => write!(f, ","),
Token::UnquoteSplicing => write!(f, ",@"),
Token::VectorStart => write!(f, "#("),
Token::Eof => write!(f, "<EOF>"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, gc::Trace, gc::Finalize)]
pub struct Position {
pub line: usize,
pub column: usize,
}
impl Position {
pub fn new() -> Self {
Position { line: 1, column: 1 }
}
}
impl Default for Position {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
pub message: String,
pub position: Position,
pub filename: Option<String>,
}
impl ParseError {
pub fn new(message: String, position: Position) -> Self {
ParseError { message, position, filename: None }
}
pub fn with_filename(message: String, position: Position, filename: String) -> Self {
ParseError { message, position, filename: Some(filename) }
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref filename) = self.filename {
write!(f, "{}:{}:E: {}", filename, self.position, self.message)
} else {
write!(f, "Parse error at {}: {}", self.position, self.message)
}
}
}
impl std::error::Error for ParseError {}
pub type ParseResult<T> = Result<T, ParseError>;
pub struct Tokenizer {
input: Vec<char>,
pos: usize,
line: usize,
column: usize,
peeked: Option<Token>,
filename: Option<String>,
}
impl Tokenizer {
pub fn new(input: &str) -> Self {
Tokenizer {
input: input.chars().collect(),
pos: 0,
line: 1,
column: 1,
peeked: None,
filename: None,
}
}
pub fn new_with_filename(input: &str, filename: String) -> Self {
Tokenizer {
input: input.chars().collect(),
pos: 0,
line: 1,
column: 1,
peeked: None,
filename: Some(filename),
}
}
fn error(&self, message: String, position: Position) -> ParseError {
if let Some(ref filename) = self.filename {
ParseError::with_filename(message, position, filename.clone())
} else {
ParseError::new(message, position)
}
}
pub fn position(&self) -> Position {
Position {
line: self.line,
column: self.column,
}
}
fn peek_char(&self) -> Option<char> {
if self.pos < self.input.len() {
Some(self.input[self.pos])
} else {
None
}
}
fn peek_char_at(&self, offset: usize) -> Option<char> {
let index = self.pos + offset;
if index < self.input.len() {
Some(self.input[index])
} else {
None
}
}
fn next_char(&mut self) -> Option<char> {
if self.pos < self.input.len() {
let ch = self.input[self.pos];
self.pos += 1;
if ch == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
Some(ch)
} else {
None
}
}
fn skip_whitespace(&mut self) {
loop {
match self.peek_char() {
Some(ch) if ch.is_whitespace() => {
self.next_char();
}
Some(';') => {
self.next_char();
while let Some(ch) = self.peek_char() {
self.next_char();
if ch == '\n' {
break;
}
}
}
Some('#') if self.peek_char_at(1) == Some('|') => {
self.next_char(); self.next_char();
let mut depth = 1;
while depth > 0 {
match self.next_char() {
Some('|') if self.peek_char() == Some('#') => {
self.next_char(); depth -= 1;
}
Some('#') if self.peek_char() == Some('|') => {
self.next_char(); depth += 1; }
Some(_) => {} None => break, }
}
}
_ => break,
}
}
}
fn is_delimiter(ch: char) -> bool {
ch.is_whitespace()
|| matches!(
ch,
'(' | ')' | '[' | ']' | '"' | ';' | ',' | '`' | '\''
)
}
fn peek_quantity_suffix(&self) -> usize {
if let Some(ch1) = self.peek_char_at(0) {
if let Some(ch2) = self.peek_char_at(1) {
match (ch1, ch2) {
('p', 't') | ('p', 'i') | ('p', 'c') | ('p', 'x') |
('i', 'n') | ('m', 'm') | ('c', 'm') | ('e', 'm') => return 2,
_ => {}
}
}
}
0
}
fn parse_number(&mut self, start_pos: Position) -> ParseResult<Token> {
let mut num_str = String::new();
while let Some(ch) = self.peek_char() {
if ch.is_ascii_digit() || matches!(ch, '.' | '+' | '-') {
num_str.push(ch);
self.next_char();
} else if matches!(ch, 'e' | 'E') {
if self.peek_char_at(1) == Some('m') {
break;
} else {
num_str.push(ch);
self.next_char();
}
} else if Self::is_delimiter(ch) {
break;
} else {
let suffix_len = self.peek_quantity_suffix();
if suffix_len > 0 {
let mut suffix = String::new();
for _ in 0..suffix_len {
if let Some(ch) = self.peek_char() {
suffix.push(ch);
}
self.next_char();
}
if let Ok(n) = num_str.parse::<f64>() {
return Ok(Token::Quantity(n, suffix));
} else if let Ok(n) = num_str.parse::<i64>() {
return Ok(Token::Quantity(n as f64, suffix));
}
return Err(self.error(
format!("Invalid quantity: {}{}", num_str, suffix),
start_pos,
));
}
return Err(self.error(
format!("Invalid character in number: {}", ch),
start_pos,
));
}
}
let suffix_len = self.peek_quantity_suffix();
if suffix_len > 0 {
let mut suffix = String::new();
for _ in 0..suffix_len {
if let Some(ch) = self.peek_char() {
suffix.push(ch);
}
self.next_char();
}
if let Ok(n) = num_str.parse::<f64>() {
return Ok(Token::Quantity(n, suffix));
} else if let Ok(n) = num_str.parse::<i64>() {
return Ok(Token::Quantity(n as f64, suffix));
}
return Err(self.error(
format!("Invalid quantity: {}{}", num_str, suffix),
start_pos,
));
}
if let Ok(n) = num_str.parse::<i64>() {
return Ok(Token::Integer(n));
}
if let Ok(n) = num_str.parse::<f64>() {
return Ok(Token::Real(n));
}
Err(self.error(
format!("Invalid number: {}", num_str),
start_pos,
))
}
fn parse_hex_number(&mut self, start_pos: Position) -> ParseResult<Token> {
let mut num_str = String::new();
while let Some(ch) = self.peek_char() {
if ch.is_ascii_hexdigit() {
num_str.push(ch);
self.next_char();
} else if Self::is_delimiter(ch) {
break;
} else {
return Err(self.error(
format!("Invalid character in hex number: {}", ch),
start_pos,
));
}
}
if num_str.is_empty() {
return Err(self.error("Empty hex number".to_string(), start_pos));
}
i64::from_str_radix(&num_str, 16)
.map(Token::Integer)
.map_err(|_| self.error(format!("Invalid hex number: {}", num_str), start_pos))
}
fn parse_octal_number(&mut self, start_pos: Position) -> ParseResult<Token> {
let mut num_str = String::new();
while let Some(ch) = self.peek_char() {
if ch.is_digit(8) {
num_str.push(ch);
self.next_char();
} else if Self::is_delimiter(ch) {
break;
} else {
return Err(self.error(
format!("Invalid character in octal number: {}", ch),
start_pos,
));
}
}
if num_str.is_empty() {
return Err(self.error("Empty octal number".to_string(), start_pos));
}
i64::from_str_radix(&num_str, 8)
.map(Token::Integer)
.map_err(|_| self.error(format!("Invalid octal number: {}", num_str), start_pos))
}
fn parse_binary_number(&mut self, start_pos: Position) -> ParseResult<Token> {
let mut num_str = String::new();
while let Some(ch) = self.peek_char() {
if matches!(ch, '0' | '1') {
num_str.push(ch);
self.next_char();
} else if Self::is_delimiter(ch) {
break;
} else {
return Err(self.error(
format!("Invalid character in binary number: {}", ch),
start_pos,
));
}
}
if num_str.is_empty() {
return Err(self.error("Empty binary number".to_string(), start_pos));
}
i64::from_str_radix(&num_str, 2)
.map(Token::Integer)
.map_err(|_| self.error(format!("Invalid binary number: {}", num_str), start_pos))
}
fn parse_symbol(&mut self) -> String {
let mut sym = String::new();
while let Some(ch) = self.peek_char() {
if Self::is_delimiter(ch) {
break;
}
sym.push(ch);
self.next_char();
}
sym
}
fn parse_string(&mut self, start_pos: Position) -> ParseResult<String> {
self.next_char();
let mut result = String::new();
loop {
match self.next_char() {
Some('"') => {
let normalized = result.replace("\r\n", "\n");
return Ok(normalized);
}
Some('\\') => {
match self.next_char() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('r') => result.push('\r'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some(ch) => result.push(ch), None => {
return Err(self.error(
"Unexpected EOF in string escape".to_string(),
start_pos,
))
}
}
}
Some(ch) => {
result.push(ch);
}
None => {
return Err(self.error(
"Unexpected EOF in string".to_string(),
start_pos,
))
}
}
}
}
fn parse_char(&mut self, start_pos: Position) -> ParseResult<char> {
if self.next_char() != Some('\\') {
return Err(self.error(
"Expected \\ after # in character literal".to_string(),
start_pos,
));
}
let mut name = String::new();
while let Some(ch) = self.peek_char() {
if Self::is_delimiter(ch) {
break;
}
name.push(ch);
self.next_char();
}
if name.is_empty() {
if let Some(ch) = self.next_char() {
return Ok(ch);
} else {
return Err(self.error(
"Unexpected end of input in character literal".to_string(),
start_pos,
));
}
}
match name.as_str() {
"space" => Ok(' '),
"newline" => Ok('\n'),
"tab" => Ok('\t'),
"return" => Ok('\r'),
s if s.starts_with("U-") => {
let hex_str = &s[2..]; u32::from_str_radix(hex_str, 16)
.ok()
.and_then(std::char::from_u32)
.ok_or_else(|| self.error(
format!("Invalid Unicode character literal: #\\{}", name),
start_pos,
))
}
s if s.chars().count() == 1 => Ok(s.chars().next().unwrap()),
_ => Err(self.error(
format!("Invalid character literal: #\\{}", name),
start_pos,
)),
}
}
fn parse_cdata_string(&mut self, start_pos: Position) -> ParseResult<Token> {
for _ in 0..9 {
self.next_char();
}
let mut content = String::new();
loop {
match self.peek_char() {
None => {
return Err(self.error(
"Unclosed CDATA section: missing ]]>".to_string(),
start_pos,
));
}
Some(']') => {
if self.pos + 2 < self.input.len()
&& self.input[self.pos] == ']'
&& self.input[self.pos + 1] == ']'
&& self.input[self.pos + 2] == '>'
{
self.next_char(); self.next_char(); self.next_char(); break;
} else {
content.push(']');
self.next_char();
}
}
Some(ch) => {
content.push(ch);
self.next_char();
}
}
}
Ok(Token::String(content))
}
pub fn next_token(&mut self) -> ParseResult<Token> {
if let Some(tok) = self.peeked.take() {
return Ok(tok);
}
self.skip_whitespace();
let start_pos = self.position();
match self.peek_char() {
None => Ok(Token::Eof),
Some('(') => {
self.next_char();
Ok(Token::LeftParen)
}
Some(')') => {
self.next_char();
Ok(Token::RightParen)
}
Some('[') => {
self.next_char();
Ok(Token::LeftBracket)
}
Some(']') => {
self.next_char();
Ok(Token::RightBracket)
}
Some('\'') => {
self.next_char();
Ok(Token::Quote)
}
Some('`') => {
self.next_char();
Ok(Token::Quasiquote)
}
Some(',') => {
self.next_char();
if self.peek_char() == Some('@') {
self.next_char();
Ok(Token::UnquoteSplicing)
} else {
Ok(Token::Unquote)
}
}
Some('"') => {
let s = self.parse_string(start_pos)?;
Ok(Token::String(s))
}
Some('#') => {
self.next_char(); match self.peek_char() {
Some('t') => {
self.next_char();
Ok(Token::Bool(true))
}
Some('f') => {
self.next_char();
Ok(Token::Bool(false))
}
Some('(') => {
self.next_char();
Ok(Token::VectorStart)
}
Some('\\') => {
let ch = self.parse_char(start_pos)?;
Ok(Token::Char(ch))
}
Some(':') => {
self.next_char(); let name = self.parse_symbol();
Ok(Token::Keyword(name))
}
Some('x') | Some('X') => {
self.next_char(); self.parse_hex_number(start_pos)
}
Some('o') | Some('O') => {
self.next_char(); self.parse_octal_number(start_pos)
}
Some('b') | Some('B') => {
self.next_char(); self.parse_binary_number(start_pos)
}
Some('!') => {
self.next_char(); let name = self.parse_symbol();
Ok(Token::Symbol(format!("#!{}", name)))
}
_ => Err(self.error(
format!("Invalid # syntax: #{:?}", self.peek_char()),
start_pos,
)),
}
}
Some(ch) if ch.is_ascii_digit() => self.parse_number(start_pos),
Some('+') | Some('-') => {
if let Some(next) = self.peek_char_at(1) {
if next.is_ascii_digit() {
self.parse_number(start_pos)
} else {
let sym = self.parse_symbol();
Ok(Token::Symbol(sym))
}
} else {
let sym = self.parse_symbol();
Ok(Token::Symbol(sym))
}
}
Some('.') => {
if let Some(next) = self.peek_char_at(1) {
if next.is_ascii_digit() {
self.parse_number(start_pos)
} else {
self.next_char();
Ok(Token::Dot)
}
} else {
self.next_char();
Ok(Token::Dot)
}
}
Some('<') => {
let cdata_prefix = ['<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['];
let is_cdata = self.pos + cdata_prefix.len() <= self.input.len()
&& self.input[self.pos..self.pos + cdata_prefix.len()] == cdata_prefix;
if is_cdata {
self.parse_cdata_string(start_pos)
} else {
let sym = self.parse_symbol();
Ok(Token::Symbol(sym))
}
}
Some(_) => {
let sym = self.parse_symbol();
if sym.ends_with(':') {
let keyword_name = sym[..sym.len()-1].to_string();
Ok(Token::Keyword(keyword_name))
} else {
Ok(Token::Symbol(sym))
}
}
}
}
pub fn peek_token(&mut self) -> ParseResult<&Token> {
if self.peeked.is_none() {
let tok = self.next_token()?;
self.peeked = Some(tok);
}
Ok(self.peeked.as_ref().unwrap())
}
}
pub struct Parser {
tokenizer: Tokenizer,
filename: Option<String>,
}
impl Parser {
pub fn new(input: &str) -> Self {
Parser {
tokenizer: Tokenizer::new(input),
filename: None,
}
}
pub fn new_with_filename(input: &str, filename: String) -> Self {
Parser {
tokenizer: Tokenizer::new_with_filename(input, filename.clone()),
filename: Some(filename),
}
}
fn error(&self, message: String, position: Position) -> ParseError {
if let Some(ref filename) = self.filename {
ParseError::with_filename(message, position, filename.clone())
} else {
ParseError::new(message, position)
}
}
pub fn parse(&mut self) -> ParseResult<Value> {
self.parse_expr()
}
pub fn peek_token(&mut self) -> ParseResult<&Token> {
self.tokenizer.peek_token()
}
pub fn current_position(&self) -> Position {
self.tokenizer.position()
}
pub fn parse_all(&mut self) -> ParseResult<Vec<Value>> {
let mut exprs = Vec::new();
loop {
let tok = self.tokenizer.peek_token()?;
if *tok == Token::Eof {
break;
}
exprs.push(self.parse_expr()?);
}
Ok(exprs)
}
fn parse_expr(&mut self) -> ParseResult<Value> {
let start_pos = self.tokenizer.position();
let tok = self.tokenizer.next_token()?;
match tok {
Token::Integer(n) => Ok(Value::integer(n)),
Token::Real(n) => Ok(Value::real(n)),
Token::Quantity(magnitude, suffix) => {
if let Some(unit) = Unit::from_suffix(&suffix) {
Ok(Value::Quantity { magnitude, unit })
} else {
Err(self.error(
format!("Invalid quantity unit: {}", suffix),
start_pos,
))
}
}
Token::String(s) => Ok(Value::string(s)),
Token::Char(ch) => Ok(Value::char(ch)),
Token::Bool(b) => Ok(Value::bool(b)),
Token::Symbol(s) => Ok(Value::symbol(&s)),
Token::Keyword(s) => Ok(Value::keyword(&s)),
Token::LeftParen | Token::LeftBracket => self.parse_list(start_pos),
Token::VectorStart => self.parse_vector(start_pos),
Token::Quote => {
let quoted = self.parse_expr()?;
Ok(Value::cons_with_pos(Value::symbol("quote"), Value::cons(quoted, Value::Nil), start_pos))
}
Token::Quasiquote => {
let quoted = self.parse_expr()?;
Ok(Value::cons_with_pos(
Value::symbol("quasiquote"),
Value::cons(quoted, Value::Nil),
start_pos
))
}
Token::Unquote => {
let quoted = self.parse_expr()?;
Ok(Value::cons_with_pos(
Value::symbol("unquote"),
Value::cons(quoted, Value::Nil),
start_pos
))
}
Token::UnquoteSplicing => {
let quoted = self.parse_expr()?;
Ok(Value::cons_with_pos(
Value::symbol("unquote-splicing"),
Value::cons(quoted, Value::Nil),
start_pos
))
}
Token::RightParen | Token::RightBracket => Err(self.error(
format!("Unexpected closing delimiter: {}", tok),
start_pos,
)),
Token::Dot => Err(self.error(
"Unexpected dot outside of list".to_string(),
start_pos,
)),
Token::Eof => Err(self.error(
"Unexpected end of input".to_string(),
start_pos,
)),
}
}
fn parse_list(&mut self, start_pos: Position) -> ParseResult<Value> {
let mut elements = Vec::new();
let mut element_positions = Vec::new();
let mut dotted_tail = None;
loop {
let tok = self.tokenizer.peek_token()?;
match tok {
Token::RightParen | Token::RightBracket => {
self.tokenizer.next_token()?; break;
}
Token::Dot => {
if elements.is_empty() {
let dot_pos = self.tokenizer.position();
return Err(self.error(
"Invalid syntax: dot cannot appear immediately after opening parenthesis\n\
Note: Identifiers cannot start with '.' (dot character is reserved for dotted pairs)\n\
Example of valid dotted pair: (a . b)\n\
Example of invalid syntax: (.gitignore)".to_string(),
dot_pos,
));
}
self.tokenizer.next_token()?;
dotted_tail = Some(self.parse_expr()?);
let tok = self.tokenizer.next_token()?;
if !matches!(tok, Token::RightParen | Token::RightBracket) {
return Err(self.error(
format!("Expected ) after dotted tail, got {}", tok),
start_pos,
));
}
break;
}
Token::Eof => {
return Err(self.error(
"Unexpected EOF in list".to_string(),
start_pos,
))
}
_ => {
let elem_pos = self.tokenizer.position();
elements.push(self.parse_expr()?);
element_positions.push(elem_pos);
}
}
}
let mut result = dotted_tail.unwrap_or(Value::Nil);
for (elem, elem_pos) in elements.into_iter().zip(element_positions.into_iter()).rev() {
result = Value::cons_with_pos(elem, result, elem_pos);
}
Ok(result)
}
fn parse_vector(&mut self, start_pos: Position) -> ParseResult<Value> {
let mut elements = Vec::new();
loop {
let tok = self.tokenizer.peek_token()?;
match tok {
Token::RightParen => {
self.tokenizer.next_token()?; break;
}
Token::Eof => {
return Err(self.error(
"Unexpected EOF in vector".to_string(),
start_pos,
))
}
_ => {
elements.push(self.parse_expr()?);
}
}
}
Ok(Value::vector(elements))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_simple() {
let mut tok = Tokenizer::new("(+ 1 2)");
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
assert_eq!(tok.next_token().unwrap(), Token::Eof);
}
#[test]
fn test_tokenize_whitespace_agnostic() {
let input = r#"(let ((x 1)
(y 2))
(+ x y))"#;
let mut tok = Tokenizer::new(input);
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("let".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("x".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("y".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
}
#[test]
fn test_tokenize_strings() {
let mut tok = Tokenizer::new(r#""hello world""#);
assert_eq!(
tok.next_token().unwrap(),
Token::String("hello world".to_string())
);
let mut tok = Tokenizer::new(r#""with\nnewline""#);
assert_eq!(
tok.next_token().unwrap(),
Token::String("with\nnewline".to_string())
);
}
#[test]
fn test_tokenize_cdata() {
let mut tok = Tokenizer::new(r#"<![CDATA[<!DOCTYPE HTML>]]>"#);
assert_eq!(
tok.next_token().unwrap(),
Token::String("<!DOCTYPE HTML>".to_string())
);
let mut tok = Tokenizer::new("<![CDATA[\nLine 1\nLine 2\n]]>");
assert_eq!(
tok.next_token().unwrap(),
Token::String("\nLine 1\nLine 2\n".to_string())
);
let mut tok = Tokenizer::new("(define x <![CDATA[test]]>)");
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("define".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Symbol("x".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::String("test".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
}
#[test]
fn test_tokenize_comments() {
let mut tok = Tokenizer::new("(+ 1 ; comment\n 2)");
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
}
#[test]
fn test_tokenize_block_comments() {
let mut tok = Tokenizer::new("(+ 1 #| block comment |# 2)");
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
}
#[test]
fn test_tokenize_booleans() {
let mut tok = Tokenizer::new("#t #f");
assert_eq!(tok.next_token().unwrap(), Token::Bool(true));
assert_eq!(tok.next_token().unwrap(), Token::Bool(false));
}
#[test]
fn test_tokenize_characters() {
let mut tok = Tokenizer::new(r#"#\a #\space #\newline"#);
assert_eq!(tok.next_token().unwrap(), Token::Char('a'));
assert_eq!(tok.next_token().unwrap(), Token::Char(' '));
assert_eq!(tok.next_token().unwrap(), Token::Char('\n'));
}
#[test]
fn test_tokenize_hex_numbers() {
let mut tok = Tokenizer::new("#xff");
assert_eq!(tok.next_token().unwrap(), Token::Integer(255));
let mut tok = Tokenizer::new("#X10");
assert_eq!(tok.next_token().unwrap(), Token::Integer(16));
let mut tok = Tokenizer::new("#xDEADBEEF");
assert_eq!(tok.next_token().unwrap(), Token::Integer(0xDEADBEEF));
let mut tok = Tokenizer::new("#x0");
assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
}
#[test]
fn test_tokenize_octal_numbers() {
let mut tok = Tokenizer::new("#o77");
assert_eq!(tok.next_token().unwrap(), Token::Integer(63));
let mut tok = Tokenizer::new("#O10");
assert_eq!(tok.next_token().unwrap(), Token::Integer(8));
let mut tok = Tokenizer::new("#o0");
assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
let mut tok = Tokenizer::new("#o777");
assert_eq!(tok.next_token().unwrap(), Token::Integer(511));
}
#[test]
fn test_tokenize_binary_numbers() {
let mut tok = Tokenizer::new("#b1010");
assert_eq!(tok.next_token().unwrap(), Token::Integer(10));
let mut tok = Tokenizer::new("#B1111");
assert_eq!(tok.next_token().unwrap(), Token::Integer(15));
let mut tok = Tokenizer::new("#b0");
assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
let mut tok = Tokenizer::new("#b11111111");
assert_eq!(tok.next_token().unwrap(), Token::Integer(255));
}
#[test]
fn test_tokenize_quote() {
let mut tok = Tokenizer::new("'(1 2)");
assert_eq!(tok.next_token().unwrap(), Token::Quote);
assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
assert_eq!(tok.next_token().unwrap(), Token::RightParen);
}
#[test]
fn test_error_positions() {
let mut tok = Tokenizer::new("(+ 1\n \"unclosed string");
tok.next_token().unwrap(); tok.next_token().unwrap(); tok.next_token().unwrap();
let err = tok.next_token().unwrap_err();
assert_eq!(err.position.line, 2); assert!(err.message.contains("EOF in string"));
}
#[test]
fn test_parse_integer() {
let mut parser = Parser::new("42");
let val = parser.parse().unwrap();
assert!(val.is_integer());
if let Value::Integer(n) = val {
assert_eq!(n, 42);
}
}
#[test]
fn test_parse_simple_list() {
let mut parser = Parser::new("(+ 1 2)");
let val = parser.parse().unwrap();
assert!(val.is_list());
if let Value::Pair(ref p) = val {
let pair = p.borrow();
assert!(pair.car.is_symbol());
}
}
#[test]
fn test_parse_nested_list() {
let mut parser = Parser::new("(+ (* 2 3) 4)");
let val = parser.parse().unwrap();
assert!(val.is_list());
}
#[test]
fn test_parse_quoted() {
let mut parser = Parser::new("'(1 2 3)");
let val = parser.parse().unwrap();
if let Value::Pair(ref p) = val {
let pair = p.borrow();
if let Value::Symbol(s) = &pair.car {
assert_eq!(&**s, "quote");
} else {
panic!("Expected symbol 'quote'");
}
} else {
panic!("Expected pair");
}
}
#[test]
fn test_parse_vector() {
let mut parser = Parser::new("#(1 2 3)");
let val = parser.parse().unwrap();
assert!(val.is_vector());
if let Value::Vector(ref v) = val {
let vec = v.borrow();
assert_eq!(vec.len(), 3);
}
}
#[test]
fn test_parse_dotted_list() {
let mut parser = Parser::new("(1 . 2)");
let val = parser.parse().unwrap();
if let Value::Pair(ref p) = val {
let pair = p.borrow();
assert!(matches!(pair.car, Value::Integer(1)));
assert!(matches!(pair.cdr, Value::Integer(2)));
} else {
panic!("Expected pair");
}
}
#[test]
fn test_parse_string() {
let mut parser = Parser::new(r#""hello world""#);
let val = parser.parse().unwrap();
assert!(val.is_string());
}
#[test]
fn test_parse_bool() {
let mut parser = Parser::new("#t");
let val = parser.parse().unwrap();
assert!(val.is_bool());
assert!(val.is_true());
let mut parser = Parser::new("#f");
let val = parser.parse().unwrap();
assert!(val.is_bool());
assert!(!val.is_true());
}
#[test]
fn test_parse_multiline_let() {
let input = r#"
(let ((x 1)
(y 2))
(+ x y))
"#;
let mut parser = Parser::new(input);
let val = parser.parse().unwrap();
assert!(val.is_list());
}
#[test]
fn test_parse_all() {
let input = "(define x 1) (define y 2) (+ x y)";
let mut parser = Parser::new(input);
let exprs = parser.parse_all().unwrap();
assert_eq!(exprs.len(), 3);
}
#[test]
fn test_parse_empty_list() {
let mut parser = Parser::new("()");
let val = parser.parse().unwrap();
assert!(val.is_nil());
}
#[test]
fn test_parse_keyword() {
let mut parser = Parser::new("#:foo");
let val = parser.parse().unwrap();
if let Value::Keyword(ref k) = val {
assert_eq!(&**k, "foo");
} else {
panic!("Expected keyword");
}
}
#[test]
fn test_parse_error_with_filename() {
let mut parser = Parser::new_with_filename("(define x", "test.scm".to_string());
let err = parser.parse().unwrap_err();
let err_string = err.to_string();
assert!(err_string.contains("test.scm"), "Error should contain filename: {}", err_string);
assert!(err_string.contains("1:1"), "Error should contain position: {}", err_string);
assert!(err_string.contains("test.scm:1:1:E:"), "Error should use OpenJade format: {}", err_string);
}
#[test]
fn test_parse_error_without_filename() {
let mut parser = Parser::new("(define x");
let err = parser.parse().unwrap_err();
let err_string = err.to_string();
assert!(!err_string.contains("test.scm"), "Error should not contain filename when not provided");
assert!(err_string.contains("1:1"), "Error should still contain position: {}", err_string);
}
#[test]
fn test_parse_error_dot_after_open_paren() {
let mut parser = Parser::new("(.gitignore)");
let err = parser.parse().unwrap_err();
let err_string = err.to_string();
assert!(err_string.contains("dot cannot appear immediately after opening parenthesis"),
"Error should mention invalid dot position: {}", err_string);
assert!(err_string.contains("Identifiers cannot start with '.'"),
"Error should explain why: {}", err_string);
}
#[test]
fn test_parse_error_dot_function_definition() {
let mut parser = Parser::new("(define (.gitignore) (list))");
let err = parser.parse().unwrap_err();
let err_string = err.to_string();
assert!(err_string.contains("dot cannot appear immediately after opening parenthesis"),
"Error should mention invalid dot position: {}", err_string);
}
#[test]
fn test_parse_valid_dotted_pair_still_works() {
let mut parser = Parser::new("(a . b)");
let val = parser.parse().unwrap();
if let Value::Pair(ref p) = val {
let pair = p.borrow();
if let Value::Symbol(s) = &pair.car {
assert_eq!(&**s, "a");
} else {
panic!("Expected symbol 'a'");
}
if let Value::Symbol(s) = &pair.cdr {
assert_eq!(&**s, "b");
} else {
panic!("Expected symbol 'b'");
}
} else {
panic!("Expected pair");
}
}
}