wip
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
16
Cargo.lock
generated
Normal file
16
Cargo.lock
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ts-parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
]
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "ts-parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
23
src/ast.rs
Normal file
23
src/ast.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use crate::token::Token;
|
||||||
|
|
||||||
|
pub struct Module {
|
||||||
|
pub statements: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Statement {
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: Token,
|
||||||
|
parameters: Vec<ParameterDeclaration>,
|
||||||
|
},
|
||||||
|
Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ParameterDeclaration {
|
||||||
|
name: Token,
|
||||||
|
typename: Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Expression {
|
||||||
|
Identifier(Token),
|
||||||
|
FunctionCall {},
|
||||||
|
}
|
51
src/format.rs
Normal file
51
src/format.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use crate::token::{CommentKind, KeywordKind, LiteralKind, Number, Token, TokenKind};
|
||||||
|
|
||||||
|
pub trait Formatter {
|
||||||
|
fn format(self, options: FormatterOptions) -> anyhow::Result<String>;
|
||||||
|
}
|
||||||
|
pub struct FormatterOptions {}
|
||||||
|
|
||||||
|
impl Formatter for &[Token] {
|
||||||
|
fn format(self, options: FormatterOptions) -> anyhow::Result<String> {
|
||||||
|
let mut result = String::new();
|
||||||
|
for t in self {
|
||||||
|
let kind = &t.kind;
|
||||||
|
let s = match kind {
|
||||||
|
TokenKind::Identifier(i) => i.clone(),
|
||||||
|
TokenKind::Literal(kind) => match kind {
|
||||||
|
LiteralKind::String(s) => {
|
||||||
|
format!("\"{s}\"")
|
||||||
|
}
|
||||||
|
LiteralKind::Number(number) => match number {
|
||||||
|
Number::Integer(i) => format!("{i}"),
|
||||||
|
Number::Float(f) => format!("{f}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TokenKind::Keyword(kind) => format!(
|
||||||
|
"{}",
|
||||||
|
match kind {
|
||||||
|
KeywordKind::function => "function ",
|
||||||
|
KeywordKind::string => "string",
|
||||||
|
KeywordKind::number => "number",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
TokenKind::Comment(kind, s) => match kind {
|
||||||
|
CommentKind::Line => format!("// {s}"),
|
||||||
|
CommentKind::Block => format!("/* {s} */"),
|
||||||
|
},
|
||||||
|
TokenKind::LeftParen => "(".to_string(),
|
||||||
|
TokenKind::RightParen => ")".to_string(),
|
||||||
|
TokenKind::LeftCurly => " {".to_string(),
|
||||||
|
TokenKind::RightCurly => "}".to_string(),
|
||||||
|
TokenKind::Comma => ", ".to_string(),
|
||||||
|
TokenKind::Colon => ": ".to_string(),
|
||||||
|
TokenKind::Semicolon => ";".to_string(),
|
||||||
|
TokenKind::Period => ".".to_string(),
|
||||||
|
TokenKind::NewLine => "\n".to_string(),
|
||||||
|
TokenKind::EndOfFile => "".to_string(),
|
||||||
|
};
|
||||||
|
result += &s;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
198
src/lexer.rs
Normal file
198
src/lexer.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
use crate::token::{CommentKind, KeywordKind, LiteralKind, Token, TokenKind, TokenLocation};
|
||||||
|
|
||||||
|
pub struct Lexer {
|
||||||
|
file: Option<String>,
|
||||||
|
source: Vec<char>,
|
||||||
|
tokens: Vec<Token>,
|
||||||
|
line: usize,
|
||||||
|
current_line_offset: usize,
|
||||||
|
start: usize,
|
||||||
|
current: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lexer {
|
||||||
|
pub fn new(source: &str, file: Option<String>) -> Lexer {
|
||||||
|
Lexer {
|
||||||
|
source: source.chars().collect::<Vec<_>>(),
|
||||||
|
tokens: Vec::new(),
|
||||||
|
line: 1,
|
||||||
|
start: 0,
|
||||||
|
current: 0,
|
||||||
|
file,
|
||||||
|
current_line_offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lex(mut self) -> Vec<Token> {
|
||||||
|
while self.current < self.source.len() {
|
||||||
|
self.start = self.current;
|
||||||
|
self.next_token();
|
||||||
|
}
|
||||||
|
self.clean_newlines();
|
||||||
|
self.tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_token(&mut self) {
|
||||||
|
let c = self.consume();
|
||||||
|
|
||||||
|
let t = match c {
|
||||||
|
'(' => Some(TokenKind::LeftParen),
|
||||||
|
')' => Some(TokenKind::RightParen),
|
||||||
|
'{' => Some(TokenKind::LeftCurly),
|
||||||
|
'}' => Some(TokenKind::RightCurly),
|
||||||
|
',' => Some(TokenKind::Comma),
|
||||||
|
':' => Some(TokenKind::Colon),
|
||||||
|
';' => Some(TokenKind::Semicolon),
|
||||||
|
'.' => Some(TokenKind::Period),
|
||||||
|
'\n' => {
|
||||||
|
self.line += 1;
|
||||||
|
self.current_line_offset = self.current;
|
||||||
|
Some(TokenKind::NewLine)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(t) = t {
|
||||||
|
self.push(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '/' {
|
||||||
|
let p = self.peek();
|
||||||
|
let t = match p {
|
||||||
|
'/' => {
|
||||||
|
while !self.is_eof() {
|
||||||
|
let c = self.consume();
|
||||||
|
if c == '\n' {
|
||||||
|
self.line += 1;
|
||||||
|
self.current_line_offset = self.current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = self.current_scan(2, 0);
|
||||||
|
TokenKind::Comment(CommentKind::Line, s)
|
||||||
|
}
|
||||||
|
'*' => {
|
||||||
|
while !self.is_eof() {
|
||||||
|
if self.peek_match("*/") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.is_eof() {
|
||||||
|
todo!("Expected */ before EOF");
|
||||||
|
}
|
||||||
|
self.current += 2;
|
||||||
|
let s = self.current_scan(2, 2);
|
||||||
|
TokenKind::Comment(CommentKind::Block, s)
|
||||||
|
}
|
||||||
|
_ => todo!("forward slash"),
|
||||||
|
};
|
||||||
|
self.push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '"' {
|
||||||
|
while !self.is_eof() {
|
||||||
|
let c = self.consume();
|
||||||
|
match c {
|
||||||
|
'"' => break,
|
||||||
|
'\n' => todo!("Expected closing string before new line"),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.is_eof() {
|
||||||
|
todo!("Expected closing string before EOL")
|
||||||
|
}
|
||||||
|
let s = self.current_scan(1, 1);
|
||||||
|
self.push(TokenKind::Literal(LiteralKind::String(s)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.is_ascii_alphabetic() {
|
||||||
|
while !self.is_eof() {
|
||||||
|
let p = self.peek();
|
||||||
|
if p.is_alphanumeric() || p == '_' {
|
||||||
|
self.consume();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.is_eof() {
|
||||||
|
todo!("Not sure if handling is necessary")
|
||||||
|
}
|
||||||
|
let s = self.current_scan(0, 0);
|
||||||
|
if let Ok(k) = TryInto::<KeywordKind>::try_into(s.as_str()) {
|
||||||
|
self.push(TokenKind::Keyword(k));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.push(TokenKind::Identifier(s));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_newlines(&mut self) {
|
||||||
|
while let Some(TokenKind::NewLine) = self.tokens.first().map(|t| &t.kind) {
|
||||||
|
self.tokens.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
let w = self
|
||||||
|
.tokens
|
||||||
|
.get(i..(i + 3))
|
||||||
|
.map(|ts| ts.iter().map(|t| &t.kind).collect::<Vec<_>>());
|
||||||
|
match w.as_deref() {
|
||||||
|
Some([TokenKind::NewLine, TokenKind::NewLine, TokenKind::NewLine]) => {
|
||||||
|
self.tokens.remove(i + 2);
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_scan(&self, start_offset: usize, end_offset: usize) -> String {
|
||||||
|
self.source[(self.start + start_offset)..(self.current - end_offset)]
|
||||||
|
.iter()
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, kind: TokenKind) {
|
||||||
|
self.tokens.push(Token {
|
||||||
|
kind,
|
||||||
|
location: TokenLocation {
|
||||||
|
file: self.file.clone(),
|
||||||
|
line: self.line,
|
||||||
|
column: self.start.saturating_sub(self.current_line_offset),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&self) -> char {
|
||||||
|
self.source[self.current]
|
||||||
|
}
|
||||||
|
fn peek_n(&self, n: usize) -> Option<char> {
|
||||||
|
self.source.get(self.current + n).copied()
|
||||||
|
}
|
||||||
|
fn peek_match(&self, m: &str) -> bool {
|
||||||
|
let c = self.current;
|
||||||
|
let s = self
|
||||||
|
.source
|
||||||
|
.get(c..(c + m.len()))
|
||||||
|
.map(|s| s.iter().collect::<String>());
|
||||||
|
if let Some(s) = s { s == m } else { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume(&mut self) -> char {
|
||||||
|
let c = self.source[self.current];
|
||||||
|
self.current += 1;
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_eof(&self) -> bool {
|
||||||
|
self.current == self.source.len()
|
||||||
|
}
|
||||||
|
}
|
40
src/main.rs
Normal file
40
src/main.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use token::Token;
|
||||||
|
|
||||||
|
mod ast;
|
||||||
|
mod format;
|
||||||
|
mod lexer;
|
||||||
|
mod parse;
|
||||||
|
mod token;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
format::Formatter,
|
||||||
|
lexer::{self, Lexer},
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASIC: &str = r#"
|
||||||
|
function hello(name: string){
|
||||||
|
console.log("Hey, ", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Starting!");
|
||||||
|
|
||||||
|
hello();
|
||||||
|
"#;
|
||||||
|
#[test]
|
||||||
|
fn lex() {
|
||||||
|
println!("Running lex");
|
||||||
|
let lexer = Lexer::new(BASIC, Some("basic.file".to_string()));
|
||||||
|
let tokens = lexer.lex();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
tokens.format(crate::format::FormatterOptions {}).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
src/parse.rs
Normal file
13
src/parse.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use crate::token::Token;
|
||||||
|
|
||||||
|
pub struct Parser {
|
||||||
|
tokens: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new(tokens: Vec<Token>) -> Parser {
|
||||||
|
Self { tokens }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&mut self) {}
|
||||||
|
}
|
71
src/token.rs
Normal file
71
src/token.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Token {
|
||||||
|
pub kind: TokenKind,
|
||||||
|
pub location: TokenLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TokenKind {
|
||||||
|
Identifier(String),
|
||||||
|
Literal(LiteralKind),
|
||||||
|
Keyword(KeywordKind),
|
||||||
|
Comment(CommentKind, String),
|
||||||
|
|
||||||
|
LeftParen,
|
||||||
|
RightParen,
|
||||||
|
LeftCurly,
|
||||||
|
RightCurly,
|
||||||
|
Comma,
|
||||||
|
Colon,
|
||||||
|
Semicolon,
|
||||||
|
Period,
|
||||||
|
NewLine,
|
||||||
|
EndOfFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CommentKind {
|
||||||
|
Line,
|
||||||
|
Block,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum KeywordKind {
|
||||||
|
function,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for KeywordKind {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"function" => Ok(Self::function),
|
||||||
|
"string" => Ok(Self::string),
|
||||||
|
"number" => Ok(Self::number),
|
||||||
|
_ => Err(anyhow!("unknown keyword")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LiteralKind {
|
||||||
|
String(String),
|
||||||
|
Number(Number),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Number {
|
||||||
|
Integer(usize),
|
||||||
|
Float(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TokenLocation {
|
||||||
|
pub file: Option<String>,
|
||||||
|
pub line: usize,
|
||||||
|
pub column: usize,
|
||||||
|
}
|
Reference in New Issue
Block a user