feat: Implement lexer and parser for AST

Introduces the lexer and parser modules, enabling the conversion of
source code into an Abstract Syntax Tree (AST).

This includes:
- Defining token kinds and structures.
- Implementing lexer logic to tokenize input.
- Defining AST node kinds and structures.
- Implementing parser logic to construct the AST from tokens.
- Adding support for basic expressions, lists, keywords, and
  identifiers.
- Adding the `lazy_static` dependency for keyword interning.
- Refactoring `Value::Nil` to `Value::Void`.
This commit is contained in:
Michael Schimmel
2026-02-17 00:15:08 +01:00
parent 3fdfd01982
commit c05c74bb65
8 changed files with 409 additions and 38 deletions
+164
View File
@@ -0,0 +1,164 @@
use std::iter::Peekable;
use std::str::Chars;
use std::sync::Arc;
use crate::ast::types::SourceLocation;
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
LeftParen, RightParen,
LeftBracket, RightBracket,
LeftBrace, RightBrace,
Quote, Backtick, Tilde, At,
Identifier(Arc<str>),
Keyword(Arc<str>),
Number(f64),
String(Arc<str>),
EOF,
}
#[derive(Debug, Clone)]
pub struct Token {
pub kind: TokenKind,
pub location: SourceLocation,
}
pub struct Lexer<'a> {
input: Peekable<Chars<'a>>,
line: u32,
col: u32,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a str) -> Self {
Self {
input: input.chars().peekable(),
line: 1,
col: 1,
}
}
pub fn next_token(&mut self) -> Result<Token, String> {
self.skip_whitespace();
let start_location = SourceLocation { line: self.line, col: self.col };
let char = match self.input.next() {
Some(c) => {
let current_char = c;
self.col += 1;
current_char
},
None => return Ok(Token { kind: TokenKind::EOF, location: start_location }),
};
let kind = match char {
'(' => TokenKind::LeftParen,
')' => TokenKind::RightParen,
'[' => TokenKind::LeftBracket,
']' => TokenKind::RightBracket,
'{' => TokenKind::LeftBrace,
'}' => TokenKind::RightBrace,
'\'' => TokenKind::Quote,
'`' => TokenKind::Backtick,
'~' => TokenKind::Tilde,
'@' => TokenKind::At,
':' => {
let id = self.read_while(|c| !c.is_whitespace() && !"()[]{}\"'`~@;".contains(c));
TokenKind::Keyword(Arc::from(id))
}
'"' => TokenKind::String(Arc::from(self.read_string()?)),
c if c.is_ascii_digit() || (c == '-' && self.peek().map_or(false, |p| p.is_ascii_digit())) => {
let mut num_str = String::from(c);
num_str.push_str(&self.read_while(|c| c.is_ascii_digit() || c == '.'));
TokenKind::Number(num_str.parse().map_err(|_| "Invalid number format")?)
}
c if is_ident_start(c) => {
let mut id = String::from(c);
id.push_str(&self.read_while(is_ident_char));
TokenKind::Identifier(Arc::from(id))
}
_ => return Err(format!("Unexpected character: {} at {}:{}", char, self.line, self.col - 1)),
};
Ok(Token { kind, location: start_location })
}
fn peek(&mut self) -> Option<&char> {
self.input.peek()
}
fn skip_whitespace(&mut self) {
while let Some(&c) = self.peek() {
if c.is_whitespace() {
if c == '\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
self.input.next();
} else if c == ';' {
while let Some(c) = self.input.next() {
if c == '\n' {
self.line += 1;
self.col = 1;
break;
}
}
} else {
break;
}
}
}
fn read_while<F>(&mut self, mut predicate: F) -> String
where F: FnMut(char) -> bool {
let mut s = String::new();
while let Some(&c) = self.peek() {
if predicate(c) {
s.push(self.input.next().unwrap());
self.col += 1;
} else {
break;
}
}
s
}
fn read_string(&mut self) -> Result<String, String> {
let mut s = String::new();
while let Some(c) = self.input.next() {
self.col += 1;
match c {
'"' => return Ok(s),
'\\' => {
let next = self.input.next().ok_or("Unterminated string escape")?;
self.col += 1;
match next {
'n' => s.push('\n'),
'r' => s.push('\r'),
't' => s.push('\t'),
'\\' => s.push('\\'),
'"' => s.push('"'),
_ => s.push(next),
}
}
'\n' => {
self.line += 1;
self.col = 1;
s.push(c);
}
_ => s.push(c),
}
}
Err("Unterminated string".to_string())
}
}
fn is_ident_start(c: char) -> bool {
c.is_alphabetic() || "+-*/<=>!$%&_?.".contains(c)
}
fn is_ident_char(c: char) -> bool {
is_ident_start(c) || c.is_ascii_digit()
}