Initial commit
- Implemented basic lexer - No spans implemented yet - No real error handling yet
This commit is contained in:
commit
f2a00e6560
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
14
Cargo.lock
generated
Normal file
14
Cargo.lock
generated
Normal file
@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "plang2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"plang2_lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plang2_lib"
|
||||
version = "0.1.0"
|
||||
4
Cargo.toml
Normal file
4
Cargo.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"plang2_lib", "plang2"
|
||||
]
|
||||
7
plang2/Cargo.toml
Normal file
7
plang2/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "plang2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
plang2_lib = { path = "../plang2_lib" }
|
||||
23
plang2/src/main.rs
Normal file
23
plang2/src/main.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#![allow(dead_code, unused)]
|
||||
use plang2_lib::*;
|
||||
|
||||
fn main() {
|
||||
|
||||
let code = r#"
|
||||
// This is the main function
|
||||
fn main() {
|
||||
let a = 5465;
|
||||
let b = 8;
|
||||
let c = a + b;
|
||||
|
||||
print_int(c);
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut lexer = Lexer::new(code);
|
||||
|
||||
let tokens = lexer.tokenize().unwrap();
|
||||
|
||||
println!("Tokens: \n{}\n", tokens);
|
||||
|
||||
}
|
||||
8
plang2_lib/Cargo.toml
Normal file
8
plang2_lib/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "plang2_lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
313
plang2_lib/src/lexer.rs
Normal file
313
plang2_lib/src/lexer.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use std::{iter::Peekable, str::CharIndices};
|
||||
|
||||
use super::token::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LexErrType {
|
||||
InvalidCharacter(char),
|
||||
InvalidEscapeChar(char),
|
||||
MissingQuoteEnd,
|
||||
}
|
||||
|
||||
// TODO: Make real errors that contain the span (offending text section with filename + line)
|
||||
#[derive(Debug)]
|
||||
pub struct LexErr {
|
||||
etype: LexErrType,
|
||||
}
|
||||
|
||||
type LexRes<T> = Result<T, LexErr>;
|
||||
|
||||
pub struct Lexer<'a> {
|
||||
// code: &'a str,
|
||||
code_iter: Peekable<CharIndices<'a>>,
|
||||
curr_char: Option<(usize, char)>,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(code: &'a str) -> Self {
|
||||
let mut code_iter = code.char_indices().peekable();
|
||||
let curr_char = code_iter.next();
|
||||
Self {
|
||||
// code,
|
||||
code_iter,
|
||||
curr_char,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tokenize(&mut self) -> LexRes<TokenStream> {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
loop {
|
||||
let (_idx, ch) = match self.curr_char {
|
||||
Some(it) => it,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let (_idx_nxt, ch_nxt) = self
|
||||
.peek()
|
||||
.map(|(a, b)| (Some(a), Some(b)))
|
||||
.unwrap_or_default();
|
||||
|
||||
match ch {
|
||||
// Skip whitespace
|
||||
' ' | '\t' | '\n' | '\r' => (),
|
||||
|
||||
// Lex tokens with 2 char length
|
||||
'/' if matches!(ch_nxt, Some('/')) => self.advance_until_new_line(),
|
||||
'=' if matches!(ch_nxt, Some('=')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Eq));
|
||||
}
|
||||
'!' if matches!(ch_nxt, Some('=')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Neq));
|
||||
}
|
||||
'>' if matches!(ch_nxt, Some('=')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Ge));
|
||||
}
|
||||
'<' if matches!(ch_nxt, Some('=')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Le));
|
||||
}
|
||||
'-' if matches!(ch_nxt, Some('>')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Arrow));
|
||||
}
|
||||
'&' if matches!(ch_nxt, Some('&')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::And));
|
||||
}
|
||||
'|' if matches!(ch_nxt, Some('|')) => {
|
||||
self.advance();
|
||||
tokens.push(Token::Op(Op::Or));
|
||||
}
|
||||
|
||||
// Lex tokens with 1 char length
|
||||
'+' => tokens.push(Token::Op(Op::Add)),
|
||||
'-' => tokens.push(Token::Op(Op::Sub)),
|
||||
'*' => tokens.push(Token::Op(Op::Mul)),
|
||||
'/' => tokens.push(Token::Op(Op::Div)),
|
||||
'%' => tokens.push(Token::Op(Op::Mod)),
|
||||
'(' => tokens.push(Token::Open(Group::Paren)),
|
||||
'[' => tokens.push(Token::Open(Group::Bracket)),
|
||||
'{' => tokens.push(Token::Open(Group::Braces)),
|
||||
')' => tokens.push(Token::Close(Group::Paren)),
|
||||
']' => tokens.push(Token::Close(Group::Bracket)),
|
||||
'}' => tokens.push(Token::Close(Group::Braces)),
|
||||
'=' => tokens.push(Token::Op(Op::Assign)),
|
||||
'>' => tokens.push(Token::Op(Op::Gt)),
|
||||
'<' => tokens.push(Token::Op(Op::Lt)),
|
||||
';' => tokens.push(Token::Semicolon),
|
||||
':' => tokens.push(Token::Colon),
|
||||
',' => tokens.push(Token::Comma),
|
||||
'.' => tokens.push(Token::Dot),
|
||||
'!' => tokens.push(Token::Op(Op::Not)),
|
||||
'^' => tokens.push(Token::Op(Op::Xor)),
|
||||
|
||||
// Lex Strings
|
||||
'"' => tokens.push(self.read_string()?),
|
||||
|
||||
// Lex numbers
|
||||
'0'..='9' => tokens.push(self.read_num()?),
|
||||
|
||||
// Lex identifiers / keywords
|
||||
'a'..='z' | 'A'..='Z' | '_' => tokens.push(self.read_ident_or_keyword()?),
|
||||
|
||||
// Anything else is an error
|
||||
_ => {
|
||||
return Err(LexErr::new(LexErrType::InvalidCharacter(ch)))
|
||||
}
|
||||
}
|
||||
|
||||
self.advance();
|
||||
}
|
||||
|
||||
Ok(TokenStream::new(tokens))
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Option<&(usize, char)> {
|
||||
self.code_iter.peek()
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.curr_char = self.code_iter.next();
|
||||
}
|
||||
|
||||
fn advance_until_new_line(&mut self) {
|
||||
while !matches!(self.curr_char, Some((_, '\n'))) {
|
||||
self.advance();
|
||||
}
|
||||
if matches!(self.curr_char, Some((_, '\r'))) {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
fn read_num(&mut self) -> LexRes<Token> {
|
||||
let mut snum = format!("{}", self.curr_char.unwrap().1);
|
||||
|
||||
while let Some((_idx, ch)) = self.peek() {
|
||||
match ch {
|
||||
'0'..='9' => snum.push(*ch),
|
||||
_ => break,
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
|
||||
// Only verified numeric chars were added so this should not fail
|
||||
// Actually it could easily fail if the number is too big
|
||||
// TODO: So this should be checked and converted into a LexErr
|
||||
Ok(Token::Literal(Literal::Int64(snum.parse().unwrap())))
|
||||
}
|
||||
|
||||
fn read_string(&mut self) -> LexRes<Token> {
|
||||
let mut text = String::new();
|
||||
|
||||
let mut escape = false;
|
||||
loop {
|
||||
let (_idx, ch) = match self.peek() {
|
||||
Some(it) => *it,
|
||||
None => return Err(LexErr::new(LexErrType::MissingQuoteEnd)),
|
||||
};
|
||||
|
||||
if escape {
|
||||
match ch {
|
||||
'"' | '\\' => text.push(ch),
|
||||
'\n' => text.push('\n'),
|
||||
'r' => text.push('\r'),
|
||||
't' => text.push('\t'),
|
||||
_ => return Err(LexErr::new(LexErrType::InvalidEscapeChar(ch))),
|
||||
}
|
||||
escape = false;
|
||||
} else {
|
||||
match ch {
|
||||
'"' => break,
|
||||
'\\' => escape = true,
|
||||
_ => text.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
self.advance();
|
||||
}
|
||||
self.advance();
|
||||
|
||||
Ok(Token::Literal(Literal::String(text)))
|
||||
}
|
||||
|
||||
fn read_ident_or_keyword(&mut self) -> LexRes<Token> {
|
||||
let mut ident = format!("{}", self.curr_char.unwrap().1);
|
||||
|
||||
while let Some((_idx, ch)) = self.peek() {
|
||||
match ch {
|
||||
'0'..='9' | 'a'..='z' | 'A'..='Z' | '_' => ident.push(*ch),
|
||||
_ => break,
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
|
||||
let token = match ident.as_str() {
|
||||
"let" => Token::Keyword(Keyword::Let),
|
||||
"if" => Token::Keyword(Keyword::If),
|
||||
"else" => Token::Keyword(Keyword::Else),
|
||||
"while" => Token::Keyword(Keyword::While),
|
||||
"loop" => Token::Keyword(Keyword::Loop),
|
||||
"fn" => Token::Keyword(Keyword::Fn),
|
||||
"return" => Token::Keyword(Keyword::Return),
|
||||
"void" => Token::Keyword(Keyword::Void),
|
||||
|
||||
"true" => Token::Literal(Literal::Boolean(true)),
|
||||
"false" => Token::Literal(Literal::Boolean(false)),
|
||||
|
||||
_ => Token::Ident(ident),
|
||||
};
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
impl LexErr {
|
||||
pub fn new(etype: LexErrType) -> Self {
|
||||
Self { etype }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
/// Try to lex a sequential string containing at least one of each tokens
|
||||
#[test]
|
||||
fn test_general() {
|
||||
let code = r#"
|
||||
// A comment
|
||||
+ -
|
||||
* / %
|
||||
== != > < >= <=
|
||||
= ->
|
||||
&& || ^ !
|
||||
([{)]}
|
||||
4564 "a string" false true
|
||||
an_5ident6
|
||||
; : , .
|
||||
let if while loop else fn return void
|
||||
"#;
|
||||
|
||||
let expected_tokens = vec![
|
||||
Token::Op(Op::Add),
|
||||
Token::Op(Op::Sub),
|
||||
|
||||
Token::Op(Op::Mul),
|
||||
Token::Op(Op::Div),
|
||||
Token::Op(Op::Mod),
|
||||
|
||||
Token::Op(Op::Eq),
|
||||
Token::Op(Op::Neq),
|
||||
Token::Op(Op::Gt),
|
||||
Token::Op(Op::Lt),
|
||||
Token::Op(Op::Ge),
|
||||
Token::Op(Op::Le),
|
||||
|
||||
Token::Op(Op::Assign),
|
||||
Token::Op(Op::Arrow),
|
||||
|
||||
Token::Op(Op::And),
|
||||
Token::Op(Op::Or),
|
||||
Token::Op(Op::Xor),
|
||||
Token::Op(Op::Not),
|
||||
|
||||
Token::Open(Group::Paren),
|
||||
Token::Open(Group::Bracket),
|
||||
Token::Open(Group::Braces),
|
||||
Token::Close(Group::Paren),
|
||||
Token::Close(Group::Bracket),
|
||||
Token::Close(Group::Braces),
|
||||
|
||||
Token::Literal(Literal::Int64(4564)),
|
||||
Token::Literal(Literal::String("a string".to_string())),
|
||||
Token::Literal(Literal::Boolean(false)),
|
||||
Token::Literal(Literal::Boolean(true)),
|
||||
|
||||
Token::Ident("an_5ident6".to_string()),
|
||||
|
||||
Token::Semicolon,
|
||||
Token::Colon,
|
||||
Token::Comma,
|
||||
Token::Dot,
|
||||
|
||||
Token::Keyword(Keyword::Let),
|
||||
Token::Keyword(Keyword::If),
|
||||
Token::Keyword(Keyword::While),
|
||||
Token::Keyword(Keyword::Loop),
|
||||
Token::Keyword(Keyword::Else),
|
||||
Token::Keyword(Keyword::Fn),
|
||||
Token::Keyword(Keyword::Return),
|
||||
Token::Keyword(Keyword::Void),
|
||||
];
|
||||
|
||||
let mut lexer = Lexer::new(code);
|
||||
let tokens = lexer.tokenize().unwrap();
|
||||
|
||||
assert_eq!(tokens.as_vec(), &expected_tokens);
|
||||
}
|
||||
}
|
||||
5
plang2_lib/src/lib.rs
Normal file
5
plang2_lib/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod token;
|
||||
pub mod lexer;
|
||||
|
||||
pub use token::*;
|
||||
pub use lexer::*;
|
||||
198
plang2_lib/src/token.rs
Normal file
198
plang2_lib/src/token.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use std::{fmt::Display, borrow::Cow};
|
||||
|
||||
/// Operators
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Op {
|
||||
// Addition
|
||||
Add,
|
||||
Sub,
|
||||
|
||||
// Multiplications
|
||||
Mul,
|
||||
Div,
|
||||
Mod,
|
||||
|
||||
// Assignment
|
||||
Assign,
|
||||
|
||||
// Equality
|
||||
Eq,
|
||||
Neq,
|
||||
Gt,
|
||||
Lt,
|
||||
Ge,
|
||||
Le,
|
||||
|
||||
// Bool
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Xor,
|
||||
|
||||
Arrow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Group {
|
||||
Paren,
|
||||
Bracket,
|
||||
Braces,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Literal {
|
||||
Boolean(bool),
|
||||
Int64(i64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Keyword {
|
||||
Let,
|
||||
While,
|
||||
Loop,
|
||||
If,
|
||||
Else,
|
||||
Fn,
|
||||
Return,
|
||||
Void,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Token {
|
||||
Literal(Literal),
|
||||
Op(Op),
|
||||
Open(Group),
|
||||
Close(Group),
|
||||
|
||||
Ident(String),
|
||||
|
||||
Keyword(Keyword),
|
||||
|
||||
Semicolon,
|
||||
Colon,
|
||||
Comma,
|
||||
Dot,
|
||||
}
|
||||
|
||||
pub struct TokenStream {
|
||||
tokens: Vec<Token>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
let op: Cow<'static, str> = match self {
|
||||
Token::Op(Op::Add) => "+".into(),
|
||||
Token::Op(Op::Sub) => "-".into(),
|
||||
|
||||
Token::Op(Op::Mul) => "*".into(),
|
||||
Token::Op(Op::Div) => "/".into(),
|
||||
Token::Op(Op::Mod) => "%".into(),
|
||||
|
||||
Token::Op(Op::Eq) => "==".into(),
|
||||
Token::Op(Op::Neq) => "!=".into(),
|
||||
Token::Op(Op::Gt) => ">".into(),
|
||||
Token::Op(Op::Lt) => "<".into(),
|
||||
Token::Op(Op::Ge) => ">=".into(),
|
||||
Token::Op(Op::Le) => "<=".into(),
|
||||
|
||||
Token::Op(Op::Assign) => "=".into(),
|
||||
Token::Op(Op::Arrow) => "->".into(),
|
||||
|
||||
Token::Op(Op::And) => "&&".into(),
|
||||
Token::Op(Op::Or) => "||".into(),
|
||||
Token::Op(Op::Xor) => "^".into(),
|
||||
Token::Op(Op::Not) => "!".into(),
|
||||
|
||||
Token::Open(Group::Paren) => "(".into(),
|
||||
Token::Open(Group::Bracket) => "[".into(),
|
||||
Token::Open(Group::Braces) => "{".into(),
|
||||
Token::Close(Group::Paren) => ")".into(),
|
||||
Token::Close(Group::Bracket) => "]".into(),
|
||||
Token::Close(Group::Braces) => "}".into(),
|
||||
|
||||
Token::Literal(Literal::Int64(num)) => format!("Int64({})", num).into(),
|
||||
Token::Literal(Literal::String(text)) => format!("String({})", text).into(),
|
||||
Token::Literal(Literal::Boolean(val)) => format!("Boolean({})", val).into(),
|
||||
|
||||
Token::Ident(ident) => format!("Ident({})", ident).into(),
|
||||
|
||||
Token::Semicolon => ";".into(),
|
||||
Token::Colon => ":".into(),
|
||||
Token::Comma => ",".into(),
|
||||
Token::Dot => ".".into(),
|
||||
|
||||
Token::Keyword(Keyword::Let) => "let".into(),
|
||||
Token::Keyword(Keyword::If) => "if".into(),
|
||||
Token::Keyword(Keyword::While) => "while".into(),
|
||||
Token::Keyword(Keyword::Loop) => "loop".into(),
|
||||
Token::Keyword(Keyword::Else) => "else".into(),
|
||||
Token::Keyword(Keyword::Fn) => "fn".into(),
|
||||
Token::Keyword(Keyword::Return) => "return".into(),
|
||||
Token::Keyword(Keyword::Void) => "void".into(),
|
||||
};
|
||||
|
||||
write!(f, "{}", op)
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenStream {
|
||||
pub fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { tokens, idx: 0 }
|
||||
}
|
||||
|
||||
pub fn as_vec(&self) -> &Vec<Token> {
|
||||
&self.tokens
|
||||
}
|
||||
|
||||
pub fn curr(&self) -> Option<&Token> {
|
||||
self.tokens.get(self.idx)
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&Token> {
|
||||
self.tokens.get(self.idx + 1)
|
||||
}
|
||||
pub fn advance(&mut self) {
|
||||
self.idx += 1
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TokenStream {
|
||||
/// Print the TokenStream with autofomatting
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut indent = 0_usize;
|
||||
let mut fresh_line = true;
|
||||
|
||||
for tok in self.tokens.iter() {
|
||||
if matches!(tok, Token::Close(Group::Braces)) {
|
||||
indent = indent.saturating_sub(1);
|
||||
fresh_line = true;
|
||||
}
|
||||
|
||||
if fresh_line {
|
||||
write!(f, "{}", " ".repeat(indent * 4))?;
|
||||
fresh_line = false;
|
||||
}
|
||||
|
||||
write!(f, "{} ", tok)?;
|
||||
|
||||
|
||||
match tok {
|
||||
Token::Open(Group::Braces) => {
|
||||
writeln!(f)?;
|
||||
indent += 1;
|
||||
fresh_line = true;
|
||||
}
|
||||
Token::Semicolon | Token::Close(Group::Braces) => {
|
||||
writeln!(f)?;
|
||||
fresh_line = true;
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user