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
Generated
+7
View File
@@ -7,6 +7,7 @@ name = "Ast"
version = "0.1.0"
dependencies = [
"eframe",
"lazy_static",
]
[[package]]
@@ -1590,6 +1591,12 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
+1
View File
@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
eframe = "0.33.3"
lazy_static = "1.4.0"
+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()
}
+4
View File
@@ -1,5 +1,9 @@
pub mod types;
pub mod nodes;
pub mod lexer;
pub mod parser;
pub use types::*;
pub use nodes::*;
pub use lexer::*;
pub use parser::*;
+4 -1
View File
@@ -23,6 +23,8 @@ pub struct Context {
/// The core AST variant enum for performance and structure
#[derive(Debug)]
pub enum UntypedKind {
/// The "..." placeholder for incomplete code
Nop,
Constant(Value),
Identifier(Arc<str>),
If {
@@ -41,6 +43,7 @@ pub enum UntypedKind {
impl Node<UntypedKind> {
pub fn eval(&self, ctx: &mut Context) -> Value {
match &self.kind {
UntypedKind::Nop => Value::Void,
UntypedKind::Constant(v) => v.clone(),
UntypedKind::Identifier(_) => todo!("Lookup in Context"),
UntypedKind::If { cond, then_br, else_br } => {
@@ -49,7 +52,7 @@ impl Node<UntypedKind> {
} else if let Some(eb) = else_br {
eb.eval(ctx)
} else {
Value::Nil
Value::Void
}
}
UntypedKind::Call { callee, args } => {
+168
View File
@@ -0,0 +1,168 @@
use std::sync::Arc;
use crate::ast::lexer::{Lexer, Token, TokenKind};
use crate::ast::types::{Identity, NodeIdentity, Value, Keyword};
use crate::ast::nodes::{Node, UntypedKind};
pub struct Parser<'a> {
lexer: Lexer<'a>,
current_token: Token,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Result<Self, String> {
let mut lexer = Lexer::new(input);
let current_token = lexer.next_token()?;
Ok(Self { lexer, current_token })
}
fn advance(&mut self) -> Result<Token, String> {
let prev = std::mem::replace(&mut self.current_token, self.lexer.next_token()?);
Ok(prev)
}
fn peek(&self) -> &TokenKind {
&self.current_token.kind
}
pub fn parse_expression(&mut self) -> Result<Node<UntypedKind>, String> {
match self.peek() {
TokenKind::LeftParen => self.parse_list(),
TokenKind::LeftBracket => self.parse_vector(),
// TokenKind::LeftBrace => self.parse_map(), // TODO
TokenKind::Quote => {
let identity = Arc::new(NodeIdentity { location: self.current_token.location });
self.parse_reader_macro(UntypedKind::Call {
callee: Box::new(self.make_id_node("quote", identity)),
args: vec![], // will be filled
})
}
_ => self.parse_atom(),
}
}
fn parse_atom(&mut self) -> Result<Node<UntypedKind>, String> {
let token = self.advance()?;
let identity = Arc::new(NodeIdentity { location: token.location });
let kind = match token.kind {
TokenKind::Number(n) => UntypedKind::Constant(Value::Float(n)),
TokenKind::String(s) => UntypedKind::Constant(Value::Text(s)),
TokenKind::Keyword(k) => UntypedKind::Constant(Value::Keyword(Keyword::intern(&k))),
TokenKind::Identifier(id) => {
match id.as_ref() {
"..." => UntypedKind::Nop,
"true" => UntypedKind::Constant(Value::Bool(true)),
"false" => UntypedKind::Constant(Value::Bool(false)),
"void" => UntypedKind::Constant(Value::Void),
_ => UntypedKind::Identifier(id),
}
}
_ => return Err(format!("Unexpected token in atom: {:?} at {:?}", token.kind, token.location)),
};
Ok(Node { identity, kind })
}
fn parse_list(&mut self) -> Result<Node<UntypedKind>, String> {
let start_loc = self.advance()?.location; // consume '('
if *self.peek() == TokenKind::RightParen {
return Err(format!("Empty list () is not a valid expression at {:?}", start_loc));
}
// Peek at head to check for special forms
let head = self.parse_expression()?;
let result = if let UntypedKind::Identifier(name) = &head.kind {
match name.as_ref() {
"if" => self.parse_if(head.identity.clone()),
_ => self.parse_call(head),
}
} else {
self.parse_call(head)
};
self.expect(TokenKind::RightParen)?;
result
}
fn parse_if(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
let cond = Box::new(self.parse_expression()?);
let then_br = Box::new(self.parse_expression()?);
let mut else_br = None;
if *self.peek() != TokenKind::RightParen {
else_br = Some(Box::new(self.parse_expression()?));
}
Ok(Node {
identity,
kind: UntypedKind::If { cond, then_br, else_br },
})
}
fn parse_call(&mut self, callee: Node<UntypedKind>) -> Result<Node<UntypedKind>, String> {
let mut args = Vec::new();
let identity = callee.identity.clone();
while *self.peek() != TokenKind::RightParen && *self.peek() != TokenKind::EOF {
args.push(self.parse_expression()?);
}
Ok(Node {
identity,
kind: UntypedKind::Call { callee: Box::new(callee), args },
})
}
fn parse_vector(&mut self) -> Result<Node<UntypedKind>, String> {
let token = self.advance()?; // consume '['
let mut elements = Vec::new();
while *self.peek() != TokenKind::RightBracket && *self.peek() != TokenKind::EOF {
// Vector elements in Myc are usually just constants in a list
let expr = self.parse_expression()?;
elements.push(expr.eval(&mut crate::ast::nodes::Context {})); // Simple direct eval for literals
}
self.expect(TokenKind::RightBracket)?;
Ok(Node {
identity: Arc::new(NodeIdentity { location: token.location }),
kind: UntypedKind::Constant(Value::List(Arc::new(elements))),
})
}
fn parse_reader_macro(&mut self, _template: UntypedKind) -> Result<Node<UntypedKind>, String> {
let token = self.advance()?; // consume '
let expr = self.parse_expression()?;
// 'x -> (quote x)
let identity = Arc::new(NodeIdentity { location: token.location });
let quote_id = self.make_id_node("quote", identity.clone());
Ok(Node {
identity,
kind: UntypedKind::Call {
callee: Box::new(quote_id),
args: vec![expr],
},
})
}
fn expect(&mut self, kind: TokenKind) -> Result<(), String> {
let token = self.advance()?;
if token.kind == kind {
Ok(())
} else {
Err(format!("Expected {:?}, but found {:?} at {:?}", kind, token.kind, token.location))
}
}
fn make_id_node(&self, name: &str, identity: Identity) -> Node<UntypedKind> {
Node {
identity,
kind: UntypedKind::Identifier(Arc::from(name)),
}
}
}
+39 -6
View File
@@ -1,12 +1,20 @@
use std::sync::Arc;
use std::collections::HashMap;
use std::fmt;
use std::sync::Mutex;
use lazy_static::lazy_static;
/// Simple source location
#[derive(Debug, Clone, Copy)]
pub struct SourceLocation {
pub line: u32,
pub col: u32,
}
/// Shared identity for nodes (Location, etc.)
#[derive(Debug, Clone)]
pub struct NodeIdentity {
pub line: u32,
pub col: u32,
pub location: SourceLocation,
}
pub type Identity = Arc<NodeIdentity>;
@@ -15,10 +23,35 @@ pub type Identity = Arc<NodeIdentity>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Keyword(pub u32);
lazy_static! {
static ref KEYWORD_REGISTRY: Mutex<HashMap<String, u32>> = Mutex::new(HashMap::new());
static ref KEYWORD_REVERSE: Mutex<Vec<String>> = Mutex::new(Vec::new());
}
impl Keyword {
pub fn intern(name: &str) -> Self {
let mut reg = KEYWORD_REGISTRY.lock().unwrap();
if let Some(&id) = reg.get(name) {
Keyword(id)
} else {
let mut rev = KEYWORD_REVERSE.lock().unwrap();
let id = rev.len() as u32;
reg.insert(name.to_string(), id);
rev.push(name.to_string());
Keyword(id)
}
}
pub fn name(&self) -> String {
let rev = KEYWORD_REVERSE.lock().unwrap();
rev[self.0 as usize].clone()
}
}
/// Core data value in Myc Script (similar to TDataValue)
#[derive(Clone)]
pub enum Value {
Nil,
Void, // Maps to vkVoid
Bool(bool),
Int(i64),
Float(f64),
@@ -32,7 +65,7 @@ pub enum Value {
impl Value {
pub fn is_truthy(&self) -> bool {
match self {
Value::Nil => false,
Value::Void => false,
Value::Bool(b) => *b,
_ => true,
}
@@ -42,12 +75,12 @@ impl Value {
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Nil => write!(f, "nil"),
Value::Void => write!(f, "void"),
Value::Bool(b) => write!(f, "{}", b),
Value::Int(i) => write!(f, "{}", i),
Value::Float(fl) => write!(f, "{}", fl),
Value::Text(t) => write!(f, "\"{}\"", t),
Value::Keyword(k) => write!(f, ":kw#{}", k.0),
Value::Keyword(k) => write!(f, ":{}", k.name()),
Value::List(l) => write!(f, "<list[{}]>", l.len()),
Value::Record(r) => write!(f, "<record[{}]>", r.len()),
Value::Function(_) => write!(f, "<fn>"),
+22 -31
View File
@@ -40,40 +40,31 @@ impl eframe::App for CompilerApp {
// Compile Button (at the top of the bottom panel)
if ui.button("Compile").clicked() {
use crate::ast::*;
use std::sync::Arc;
use crate::ast::parser::Parser;
use crate::ast::nodes::Context;
// 1. Create shared identity
let identity = Arc::new(NodeIdentity { line: 1, col: 1 });
// 2. Build AST: (if true 42 nil)
let ast = Node {
identity: identity.clone(),
kind: UntypedKind::If {
cond: Box::new(Node {
identity: identity.clone(),
kind: UntypedKind::Constant(Value::Bool(true)),
}),
then_br: Box::new(Node {
identity: identity.clone(),
kind: UntypedKind::Constant(Value::Int(42)),
}),
else_br: Some(Box::new(Node {
identity: identity.clone(),
kind: UntypedKind::Constant(Value::Nil),
})),
},
let mut parser = match Parser::new(&self.source_code) {
Ok(p) => p,
Err(e) => {
self.output_log = format!("Lexer Error: {}", e);
return;
}
};
// 3. Evaluate
let mut context = Context {};
let result = ast.eval(&mut context);
self.output_log = format!(
"AST Test:\nSource: (if true 42 nil)\nResult: {}\n\nCompiler log:\nStarted at {:?}",
result,
std::time::SystemTime::now(),
);
match parser.parse_expression() {
Ok(ast) => {
let mut context = Context {};
let result = ast.eval(&mut context);
self.output_log = format!(
"AST Parsed Successfully.\nResult: {}\n\nFinished at {:?}",
result,
std::time::SystemTime::now(),
);
}
Err(e) => {
self.output_log = format!("Parser Error: {}", e);
}
}
}
ui.add_space(5.0);