mirror of
https://git.fddl.dev/fddl/fddl.git
synced 2024-12-24 21:30:26 +00:00
initial commit - lexer builds, tests run
This commit is contained in:
commit
e8b8d41baa
15 changed files with 725 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "fiddle"
|
||||
version = "0.1.0"
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "fiddle"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Tristan Smith <tristan.smith@pm.me>"]
|
||||
description = "A small programming language written in Rust."
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[dependencies]
|
75
readme.md
Normal file
75
readme.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Fiddle Programming Language
|
||||
|
||||
Fiddle is a small programming language inspired by various languages, designed to help learn language implementation concepts in Rust.
|
||||
|
||||
I have, off and on throughout the last 15 or so years attempted to learn a programming language of some sort. I could always get through the basics, but would get stuck with any real world projects. And I wouldn't know who to turn to even if I knew where to start.
|
||||
|
||||
So I started learning Rust and really like it. So I've been following some tutorials and the Crafting Interpretors site as guides for this very problematic programming language.
|
||||
|
||||
I like aspects of so many programming languages, but I don't really like any of them, so I always found it hard to pick one and stick with it. But I had the same problem playing World of Warcraft, too.
|
||||
|
||||
So I, like many of you, decided to make a hobby programming language to see what may be able to be done with it.
|
||||
|
||||
## Features
|
||||
|
||||
- Custom syntax with unique operators and keywords
|
||||
- Documentation comments using `#`, similar to Rust's style
|
||||
- Lexer and parser built from scratch in Rust
|
||||
|
||||
## Getting Started
|
||||
|
||||
To run the REPL:
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
To run a fiddle script:
|
||||
|
||||
```sh
|
||||
cargo run path/to/script.fddl
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
##! This is a sample module
|
||||
|
||||
module math {
|
||||
|
||||
### Computes the square of a number
|
||||
func square(x) => x ^ 2;
|
||||
}
|
||||
|
||||
define $number := 5;
|
||||
print(`The square of $number is ${math.square($number)}`);
|
||||
```
|
||||
|
||||
(At least for right now.)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **Notes and Next Steps**
|
||||
|
||||
- [x] Added first new set of tokens and features, added the first lexer tests.
|
||||
- [ ] `parser` module is a placeholder.
|
||||
- [ ] `interpreter` module is a placeholder.
|
||||
- [ ] Implement a more robust error handling mechanism instead of using `stderr`.
|
||||
- [ ] Imlement string interpolation (backticks with `$variable`)
|
||||
- [ ] Continue to expand tests to cover all new syntax and features.
|
||||
|
||||
---
|
||||
|
||||
## **Running the Project**
|
||||
|
||||
Make sure your project compiles and the tests pass:
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
3
src/interpreter/eval.rs
Normal file
3
src/interpreter/eval.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Placeholder for interpreter implementation
|
||||
|
||||
// not even close yet
|
3
src/interpreter/mod.rs
Normal file
3
src/interpreter/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
//pub mod eval;
|
||||
|
||||
//pub use eval::*;
|
366
src/lexer/lexer.rs
Normal file
366
src/lexer/lexer.rs
Normal file
|
@ -0,0 +1,366 @@
|
|||
use crate::lexer::token::Token;
|
||||
|
||||
pub struct Lexer {
|
||||
source: Vec<char>,
|
||||
start: usize,
|
||||
current: usize,
|
||||
line: usize,
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
pub fn new(source: String) -> Self {
|
||||
Lexer {
|
||||
source: source.chars().collect(),
|
||||
start: 0,
|
||||
current: 0,
|
||||
line: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan_tokens(&mut self) -> Vec<Token> {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
while !self.is_at_end() {
|
||||
self.start = self.current;
|
||||
if let Some(token) = self.scan_token() {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push(Token::EOF);
|
||||
tokens
|
||||
}
|
||||
|
||||
fn scan_token(&mut self) -> Option<Token> {
|
||||
let c = self.advance();
|
||||
|
||||
match c {
|
||||
'#' => self.handle_comment_or_doc(),
|
||||
'(' => Some(Token::LeftParen),
|
||||
')' => Some(Token::RightParen),
|
||||
'{' => Some(Token::LeftBrace),
|
||||
'}' => Some(Token::RightBrace),
|
||||
',' => Some(Token::Comma),
|
||||
'.' => Some(Token::Dot),
|
||||
'-' => Some(Token::Minus),
|
||||
'+' => Some(Token::Plus),
|
||||
';' => Some(Token::Semicolon),
|
||||
'*' => Some(Token::Star),
|
||||
'%' => Some(Token::Percent),
|
||||
'^' => Some(Token::Caret),
|
||||
'~' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::TildeEqual)
|
||||
} else {
|
||||
Some(Token::Tilde)
|
||||
}
|
||||
},
|
||||
'`' => Some(Token::Backtick),
|
||||
'$' => Some(Token::Dollar),
|
||||
'@' => Some(Token::At),
|
||||
'?' => Some(Token::Question),
|
||||
'!' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::BangEqual)
|
||||
} else {
|
||||
Some(Token::Exclamation)
|
||||
}
|
||||
},
|
||||
'|' => {
|
||||
if self.match_char('|') {
|
||||
Some(Token::DoublePipe)
|
||||
} else {
|
||||
Some(Token::Pipe)
|
||||
}
|
||||
},
|
||||
'&' => {
|
||||
if self.match_char('&') {
|
||||
Some(Token::DoubleAmpersand)
|
||||
} else {
|
||||
Some(Token::Ampersand)
|
||||
}
|
||||
},
|
||||
'=' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::EqualEqual)
|
||||
} else if self.match_char('>') {
|
||||
Some(Token::FatArrow)
|
||||
} else {
|
||||
Some(Token::Equal)
|
||||
}
|
||||
},
|
||||
':' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::ColonEqual)
|
||||
} else {
|
||||
// Handle single ':' if needed
|
||||
Some(Token::Colon)
|
||||
}
|
||||
},
|
||||
'<' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::LessEqual)
|
||||
} else {
|
||||
Some(Token::Less)
|
||||
}
|
||||
},
|
||||
'>' => {
|
||||
if self.match_char('=') {
|
||||
Some(Token::GreaterEqual)
|
||||
} else {
|
||||
Some(Token::Greater)
|
||||
}
|
||||
},
|
||||
'/' => {
|
||||
if self.match_char('/') {
|
||||
// It's a comment, consume until end of line
|
||||
let mut comment = String::new();
|
||||
while self.peek() != '\n' && !self.is_at_end() {
|
||||
comment.push(self.advance());
|
||||
}
|
||||
Some(Token::Comment(comment))
|
||||
} else {
|
||||
Some(Token::Slash)
|
||||
}
|
||||
},
|
||||
' ' | '\r' | '\t' => None, // Ignore whitespace
|
||||
'\n' => {
|
||||
self.line += 1;
|
||||
None
|
||||
},
|
||||
'"' => self.string(),
|
||||
c if c.is_ascii_digit() => self.number(),
|
||||
c if self.is_alpha(c) => self.identifier(),
|
||||
_ => {
|
||||
eprintln!("Unexpected character '{}' on line {}", c, self.line);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
fn advance(&mut self) -> char {
|
||||
let c = if self.is_at_end() {
|
||||
'\0'
|
||||
} else {
|
||||
self.source[self.current]
|
||||
};
|
||||
self.current += 1;
|
||||
c
|
||||
}
|
||||
|
||||
fn match_char(&mut self, expected: char) -> bool {
|
||||
if self.is_at_end() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.source[self.current] != expected {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current += 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn peek(&self) -> char {
|
||||
if self.is_at_end() {
|
||||
'\0'
|
||||
} else {
|
||||
self.source[self.current]
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_next(&self) -> char {
|
||||
if self.current + 1 >= self.source.len() {
|
||||
'\0'
|
||||
} else {
|
||||
self.source[self.current + 1]
|
||||
}
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
self.current >= self.source.len()
|
||||
}
|
||||
|
||||
fn string(&mut self) -> Option<Token> {
|
||||
while self.peek() != '"' && !self.is_at_end() {
|
||||
if self.peek() == '\n' {
|
||||
self.line += 1;
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
|
||||
// Check if we've reached the end without finding a closing quote
|
||||
if self.is_at_end() {
|
||||
eprintln!("Unterminated string on line {}", self.line);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Consume the closing quote
|
||||
self.advance();
|
||||
|
||||
// Extract the string value
|
||||
let value: String = self.source[self.start + 1..self.current - 1]
|
||||
.iter()
|
||||
.collect();
|
||||
Some(Token::StringLiteral(value))
|
||||
}
|
||||
|
||||
fn number(&mut self) -> Option<Token> {
|
||||
while self.peek().is_ascii_digit() {
|
||||
self.advance();
|
||||
}
|
||||
|
||||
// Look for a fractional part
|
||||
if self.peek() == '.' && self.peek_next().is_ascii_digit() {
|
||||
// Consume the '.'
|
||||
self.advance();
|
||||
|
||||
while self.peek().is_ascii_digit() {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
let value_str: String = self.source[self.start..self.current]
|
||||
.iter()
|
||||
.collect();
|
||||
let value = value_str.parse::<f64>().unwrap();
|
||||
Some(Token::Number(value))
|
||||
}
|
||||
|
||||
fn identifier(&mut self) -> Option<Token> {
|
||||
while self.is_alphanumeric(self.peek()) || self.peek() == '_' {
|
||||
self.advance();
|
||||
}
|
||||
|
||||
let text: String = self.source[self.start..self.current]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
// Check for reserved keywords
|
||||
let token = match text.as_str() {
|
||||
"and" => Token::And,
|
||||
"class" => Token::Class,
|
||||
"else" => Token::Else,
|
||||
"false" => Token::False,
|
||||
"func" => Token::Func,
|
||||
"for" => Token::For,
|
||||
"if" => Token::If,
|
||||
"nil" => Token::Nil,
|
||||
"or" => Token::Or,
|
||||
"print" => Token::Print,
|
||||
"return" => Token::Return,
|
||||
"super" => Token::Super,
|
||||
"this" => Token::This,
|
||||
"true" => Token::True,
|
||||
"let" => Token::Let,
|
||||
"while" => Token::While,
|
||||
"const" => Token::Const,
|
||||
"define" => Token::Define,
|
||||
"lambda" => Token::Lambda,
|
||||
"match" => Token::Match,
|
||||
"case" => Token::Case,
|
||||
"switch" => Token::Switch,
|
||||
"until" => Token::Until,
|
||||
"repeat" => Token::Repeat,
|
||||
"unless" => Token::Unless,
|
||||
"yes" => Token::Yes,
|
||||
"no" => Token::No,
|
||||
"on" => Token::On,
|
||||
"off" => Token::Off,
|
||||
"module" => Token::Module,
|
||||
_ => Token::Identifier(text),
|
||||
};
|
||||
|
||||
Some(token)
|
||||
}
|
||||
|
||||
fn is_alpha(&self, c: char) -> bool {
|
||||
c.is_alphabetic() || c == '_'
|
||||
}
|
||||
|
||||
fn is_alphanumeric(&self, c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_'
|
||||
}
|
||||
|
||||
fn line_comment(&mut self) {
|
||||
while self.peek() != '\n' && !self.is_at_end() {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
fn block_comment(&mut self) {
|
||||
while !self.is_at_end() {
|
||||
if self.peek() == '*' && self.peek_next() == '/' {
|
||||
self.advance();
|
||||
self.advance();
|
||||
break;
|
||||
} else {
|
||||
if self.peek() == '\n' {
|
||||
self.line += 1;
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_comment_or_doc(&mut self) -> Option<Token> {
|
||||
// We have matched one '#' character so far
|
||||
let mut count = 1;
|
||||
|
||||
// Count additional consecutive '#' characters
|
||||
while self.match_char('#') {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Check for an exclamation mark after the '#' characters
|
||||
let has_exclamation = self.match_char('!');
|
||||
|
||||
match (count, has_exclamation) {
|
||||
(1, _) => {
|
||||
// Single '#' - Line comment
|
||||
self.line_comment();
|
||||
None
|
||||
}
|
||||
(2, true) => {
|
||||
// '##!' - Module-level documentation comment
|
||||
self.doc_comment("module")
|
||||
}
|
||||
(2, false) => {
|
||||
// '##' - Block comment
|
||||
self.block_comment();
|
||||
None
|
||||
}
|
||||
(3, _) => {
|
||||
// '###' - Item-level documentation comment
|
||||
self.doc_comment("item")
|
||||
}
|
||||
(n, _) if n >= 4 => {
|
||||
// '####' or more - Block comment
|
||||
self.block_comment();
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
// Fallback to line comment
|
||||
self.line_comment();
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn doc_comment(&mut self, _kind: &str) -> Option<Token> {
|
||||
let mut comment = String::new();
|
||||
while self.peek() != '\n' && !self.is_at_end() {
|
||||
comment.push(self.advance());
|
||||
}
|
||||
|
||||
// Consume the newline character
|
||||
if self.peek() == '\n' {
|
||||
self.advance();
|
||||
}
|
||||
|
||||
Some(Token::DocComment(comment.trim().to_string()))
|
||||
}
|
||||
|
||||
}
|
5
src/lexer/mod.rs
Normal file
5
src/lexer/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod lexer;
|
||||
pub mod token;
|
||||
|
||||
pub use lexer::Lexer;
|
||||
// pub use token::Token;
|
84
src/lexer/token.rs
Normal file
84
src/lexer/token.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Token {
|
||||
// Single-character tokens
|
||||
LeftParen, // (
|
||||
RightParen, // )
|
||||
LeftBrace, // {
|
||||
RightBrace, // }
|
||||
Comma, // ,
|
||||
Dot, // .
|
||||
Minus, // -
|
||||
Plus, // +
|
||||
Semicolon, // ;
|
||||
Colon, // :
|
||||
Slash, // /
|
||||
Star, // *
|
||||
Percent, // %
|
||||
Caret, // ^
|
||||
Tilde, // ~
|
||||
Backtick, // `
|
||||
Dollar, // $
|
||||
At, // @
|
||||
// Hash, // #
|
||||
Question, // ?
|
||||
Exclamation, // !
|
||||
Pipe, // |
|
||||
Ampersand, // &
|
||||
|
||||
// one or two character tokens
|
||||
BangEqual, // !=
|
||||
Equal, // =
|
||||
EqualEqual, // ==
|
||||
Greater, // >
|
||||
GreaterEqual, // >=
|
||||
Less, // <
|
||||
LessEqual, // <=
|
||||
FatArrow, // =>
|
||||
ColonEqual, // :=
|
||||
TildeEqual, // ~=
|
||||
DoublePipe, // ||
|
||||
DoubleAmpersand, // &&
|
||||
|
||||
// Literals
|
||||
Identifier(String),
|
||||
StringLiteral(String),
|
||||
Number(f64),
|
||||
|
||||
// Keywords
|
||||
And,
|
||||
Class,
|
||||
Else,
|
||||
False,
|
||||
Func,
|
||||
For,
|
||||
If,
|
||||
Nil,
|
||||
Or,
|
||||
Print,
|
||||
Return,
|
||||
Super,
|
||||
This,
|
||||
True,
|
||||
Let,
|
||||
While,
|
||||
Const,
|
||||
Define,
|
||||
Lambda,
|
||||
Match,
|
||||
Case,
|
||||
Switch,
|
||||
Until,
|
||||
Repeat,
|
||||
Unless,
|
||||
Yes,
|
||||
No,
|
||||
On,
|
||||
Off,
|
||||
Module,
|
||||
|
||||
// Documentation and comments
|
||||
DocComment(String), // ##!, ###
|
||||
Comment(String), // #
|
||||
|
||||
EOF,
|
||||
}
|
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod interpreter;
|
||||
|
||||
// ohhhhh, this file puts your created files together
|
53
src/main.rs
Normal file
53
src/main.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
mod lexer;
|
||||
mod parser;
|
||||
mod interpreter;
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use lexer::Lexer;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() > 1 {
|
||||
// If a file is provided, run it
|
||||
run_file(&args[1]);
|
||||
} else {
|
||||
// Otherwise start the REPL
|
||||
run_repl();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_repl() {
|
||||
println!("fiddle REPL");
|
||||
loop {
|
||||
print!("> ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer).unwrap();
|
||||
|
||||
if buffer.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
run(buffer.clone());
|
||||
}
|
||||
}
|
||||
fn run_file(path: &str) {
|
||||
let source = fs::read_to_string(path).expect("Failed to read source file");
|
||||
run(source);
|
||||
}
|
||||
|
||||
fn run(source: String) {
|
||||
let mut lexer = Lexer::new(source);
|
||||
let tokens = lexer.scan_tokens();
|
||||
|
||||
for token in tokens {
|
||||
println!("{:?}", token);
|
||||
}
|
||||
|
||||
// pass tokens to parser and interpreter
|
||||
}
|
9
src/parser/ast.rs
Normal file
9
src/parser/ast.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// placeholder for ast defintions
|
||||
|
||||
pub enum Expression {
|
||||
// Define expression types
|
||||
}
|
||||
|
||||
pub enum Statement {
|
||||
// Define statement types
|
||||
}
|
4
src/parser/mod.rs
Normal file
4
src/parser/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
// pub mod ast;
|
||||
|
||||
// pub use ast::*;
|
||||
// don't fully understand this re-export
|
101
tests/lexer_tests.rs
Normal file
101
tests/lexer_tests.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use fiddle::lexer::Lexer;
|
||||
use fiddle::lexer::token::Token;
|
||||
|
||||
#[test]
|
||||
fn test_single_tokens() {
|
||||
let source = String::from("()+-*/;");
|
||||
let mut lexer = Lexer::new(source);
|
||||
let tokens = lexer.scan_tokens();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
vec![
|
||||
Token::LeftParen,
|
||||
Token::RightParen,
|
||||
Token::Plus,
|
||||
Token::Minus,
|
||||
Token::Star,
|
||||
Token::Slash,
|
||||
Token::Semicolon,
|
||||
Token::EOF
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identifier_and_keywords() {
|
||||
let source = String::from("let $varName := 123; ");
|
||||
let mut lexer = Lexer::new(source);
|
||||
let tokens = lexer.scan_tokens();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
vec![
|
||||
Token::Let,
|
||||
Token::Dollar,
|
||||
Token::Identifier("varName".to_string()),
|
||||
Token::ColonEqual,
|
||||
Token::Number(123.0),
|
||||
Token::Semicolon,
|
||||
Token::EOF
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_doc_comments() {
|
||||
let source = String::from("##! Module documentation
|
||||
module test {
|
||||
### Function documentation
|
||||
func example() {
|
||||
# Regular comment
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
");
|
||||
let mut lexer = Lexer::new(source);
|
||||
let tokens = lexer.scan_tokens();
|
||||
|
||||
println!("Tokens: {:?}", tokens);
|
||||
|
||||
assert_eq!(tokens[0], Token::DocComment("Module documentation".to_string()));
|
||||
assert_eq!(tokens[1], Token::Module);
|
||||
assert_eq!(tokens[2], Token::Identifier("test".to_string()));
|
||||
assert_eq!(tokens[3], Token::LeftBrace);
|
||||
assert_eq!(tokens[4], Token::DocComment("Function documentation".to_string()));
|
||||
assert_eq!(tokens[5], Token::Func);
|
||||
assert_eq!(tokens[6], Token::Identifier("example".to_string()));
|
||||
assert_eq!(tokens[7], Token::LeftParen);
|
||||
assert_eq!(tokens[8], Token::RightParen);
|
||||
assert_eq!(tokens[9], Token::LeftBrace);
|
||||
assert_eq!(tokens[10], Token::Return);
|
||||
assert_eq!(tokens[11], Token::Number(42.0));
|
||||
assert_eq!(tokens[12], Token::Semicolon);
|
||||
assert_eq!(tokens[13], Token::RightBrace); // Closes function body
|
||||
assert_eq!(tokens[14], Token::RightBrace); // Closes module
|
||||
assert_eq!(tokens[15], Token::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tilde_operator() {
|
||||
let source = String::from("if (a ~= b) { ~c }");
|
||||
let mut lexer = Lexer::new(source);
|
||||
let tokens = lexer.scan_tokens();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
vec![
|
||||
Token::If,
|
||||
Token::LeftParen,
|
||||
Token::Identifier("a".to_string()),
|
||||
Token::TildeEqual,
|
||||
Token::Identifier("b".to_string()),
|
||||
Token::RightParen,
|
||||
Token::LeftBrace,
|
||||
Token::Tilde,
|
||||
Token::Identifier("c".to_string()),
|
||||
Token::RightBrace,
|
||||
Token::EOF
|
||||
]
|
||||
);
|
||||
}
|
0
tests/parser_tests.rs
Normal file
0
tests/parser_tests.rs
Normal file
Loading…
Reference in a new issue