initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
534
Cargo.lock
generated
Normal file
534
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rush"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0"
|
||||
predicates = "1.0"
|
||||
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# rush - A Modern Shell Scripting Language Interpreter
|
||||
|
||||
## Overview
|
||||
Rush is a modern shell scripting language interpreter designed to execute scripts written in the `.rsh` format. It supports features such as variable assignment, control flow, and parallel execution, making it a powerful tool for automating tasks and scripting.
|
||||
|
||||
More info coming soon
|
||||
14
src/error/mod.rs
Normal file
14
src/error/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#[derive(Debug)]
|
||||
pub enum RushError {
|
||||
SyntaxError(String),
|
||||
RuntimeError(String),
|
||||
VariableError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RushError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RushError {}
|
||||
367
src/interpreter/executor.rs
Normal file
367
src/interpreter/executor.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use crate::parser::ast::{AstNode, Command, Statement};
|
||||
use crate::runtime::builtins;
|
||||
use crate::runtime::environment::Environment;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub struct Executor<'a> {
|
||||
env: &'a mut Environment,
|
||||
}
|
||||
|
||||
impl<'a> Executor<'a> {
|
||||
pub fn new(env: &'a mut Environment) -> Self {
|
||||
Executor { env }
|
||||
}
|
||||
|
||||
pub fn execute_node(&mut self, node: AstNode) {
|
||||
match node {
|
||||
AstNode::VariableAssignment { name, value } => {
|
||||
if let AstNode::Literal(val) = *value {
|
||||
self.env.set_variable(name, val);
|
||||
}
|
||||
}
|
||||
AstNode::Command { name, args } => {
|
||||
// convert args to strings
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if its a variable reference
|
||||
if s.starts_with('$') {
|
||||
self.env.get_variable(&s[1..]).map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// built in commands :D
|
||||
//todo: move and add more builtins
|
||||
if name == "echo" {
|
||||
builtins::echo(arg_strings);
|
||||
} else if name == "exit" {
|
||||
let code = arg_strings
|
||||
.first()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
builtins::exit(code);
|
||||
} else if name == "require_root" {
|
||||
builtins::require_root();
|
||||
} else {
|
||||
// execute external cmd
|
||||
let cmd = Command {
|
||||
name: name.clone(),
|
||||
args: arg_strings,
|
||||
};
|
||||
self.execute_command(cmd);
|
||||
}
|
||||
}
|
||||
AstNode::ControlFlow {
|
||||
condition: _,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
println!("TODO: Execute if statement");
|
||||
for node in then_branch {
|
||||
self.execute_node(node);
|
||||
}
|
||||
if let Some(else_nodes) = else_branch {
|
||||
for node in else_nodes {
|
||||
self.execute_node(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::For { var, items, body } => {
|
||||
// execute for loop by iterating over items
|
||||
for item in items {
|
||||
// set loop variable
|
||||
self.env.set_variable(var.clone(), item);
|
||||
|
||||
// Execute body
|
||||
for node in &body {
|
||||
self.execute_node(node.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Parallel { blocks } => {
|
||||
// create multithreaded runtime
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4) //todo: make configurable or dynamic
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async {
|
||||
let mut handles = vec![];
|
||||
for block in blocks {
|
||||
// clone the environment for this block to access variables
|
||||
let env = self.env.clone();
|
||||
|
||||
// Spawn each block as a separate task (can run on different threads)
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
for node in block {
|
||||
// Execute node
|
||||
if let AstNode::Command { name, args } = node {
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if its a variable reference
|
||||
if s.starts_with('$') {
|
||||
env.get_variable(&s[1..]).map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
} else {
|
||||
// execute external cmd
|
||||
match std::process::Command::new(&name)
|
||||
.args(&arg_strings)
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// wait for all
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
AstNode::Workers { count, body } => {
|
||||
// execute with limited concurrency using a semaphore and multi-threaded runtime
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(count.max(2)) // use at least 2 threads, or the worker count
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
// clone environment for workers
|
||||
let env = self.env.clone();
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(count));
|
||||
let mut handles = vec![];
|
||||
|
||||
for node in body {
|
||||
let sem = semaphore.clone();
|
||||
let env_clone = env.clone();
|
||||
|
||||
// clone the node for the async task
|
||||
let node_clone = node.clone();
|
||||
|
||||
// Use spawn_blocking for CPU bound work to run on thread pool
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
// Acquire semaphore permit (limits concurrency)
|
||||
// Note: We need to do this in a blocking context
|
||||
let rt_inner = tokio::runtime::Handle::current();
|
||||
let _permit = rt_inner.block_on(async { sem.acquire().await.unwrap() });
|
||||
|
||||
// execute the node based on its type
|
||||
match node_clone {
|
||||
AstNode::Command { name, args } => {
|
||||
let arg_strings: Vec<String> = args
|
||||
.into_iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
// Check if it's a variable reference
|
||||
if s.starts_with('$') {
|
||||
env_clone
|
||||
.get_variable(&s[1..])
|
||||
.map(|v| v.clone())
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
} else {
|
||||
match std::process::Command::new(&name)
|
||||
.args(&arg_strings)
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!(
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::For {
|
||||
var,
|
||||
items,
|
||||
body: for_body,
|
||||
} => {
|
||||
// execute for loop
|
||||
for item in items {
|
||||
for for_node in &for_body {
|
||||
// simple execution for commands in for loops
|
||||
if let AstNode::Command { name, args } = for_node {
|
||||
let arg_strings: Vec<String> = args
|
||||
.iter()
|
||||
.filter_map(|arg| {
|
||||
if let AstNode::Literal(s) = arg {
|
||||
|
||||
if s.starts_with('$') {
|
||||
let var_name = &s[1..];
|
||||
if var_name == var {
|
||||
// loop var
|
||||
Some(item.clone())
|
||||
} else {
|
||||
// Environment var
|
||||
env_clone
|
||||
.get_variable(var_name)
|
||||
.map(|v| v.clone())
|
||||
}
|
||||
} else {
|
||||
Some(s.clone())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if name == "echo" {
|
||||
println!("{}", arg_strings.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//todo: other node types not yet supported in workers
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// wait for all tasks to complete
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
AstNode::Literal(_) => {
|
||||
// literals dont execute on their own
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, statement: Statement) {
|
||||
match statement {
|
||||
Statement::Command(cmd) => self.execute_command(cmd),
|
||||
Statement::Assignment(var, value) => self.env.set_variable(var, value),
|
||||
Statement::For { var, items, body } => {
|
||||
// todo: Implement for loop execution
|
||||
println!(
|
||||
"todo: Execute for loop with var '{}' over {} items",
|
||||
var,
|
||||
items.len()
|
||||
);
|
||||
for _stmt in body {
|
||||
// execute each statement in the loop body
|
||||
}
|
||||
}
|
||||
Statement::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
// todo: Implement if statement execution
|
||||
println!("todo: Execute if with condition '{}'", condition);
|
||||
for _stmt in then_branch {
|
||||
// Execute then branch
|
||||
}
|
||||
if let Some(else_stmts) = else_branch {
|
||||
for _stmt in else_stmts {
|
||||
// Execute else branch
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::Parallel { blocks } => {
|
||||
// todo: Implement parallel execution
|
||||
println!("todo: Execute {} parallel blocks", blocks.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_command(&mut self, command: Command) {
|
||||
let result = StdCommand::new(&command.name).args(&command.args).output();
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Command '{}' failed with exit code: {:?}",
|
||||
command.name,
|
||||
output.status.code()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute command '{}': {}", command.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/interpreter/mod.rs
Normal file
32
src/interpreter/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
pub mod executor;
|
||||
pub mod parallel;
|
||||
|
||||
use crate::parser::Parser;
|
||||
use crate::runtime::environment::Environment;
|
||||
use executor::Executor;
|
||||
|
||||
/// Main entry point for executing rush scripts
|
||||
pub fn execute(content: &str) {
|
||||
// create a new parser
|
||||
let parser = Parser::new();
|
||||
|
||||
// parse the script content
|
||||
let program = match parser.parse(content) {
|
||||
Ok(prog) => prog,
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// create a new environment
|
||||
let mut env = Environment::new();
|
||||
|
||||
// create an executor
|
||||
let mut executor = Executor::new(&mut env);
|
||||
|
||||
// execute each statement in the program
|
||||
for statement in program.statements {
|
||||
executor.execute_node(statement);
|
||||
}
|
||||
}
|
||||
39
src/interpreter/parallel.rs
Normal file
39
src/interpreter/parallel.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use tokio::task;
|
||||
|
||||
pub async fn execute_parallel<F, Fut>(tasks: Vec<F>)
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let handles: Vec<_> = tasks
|
||||
.into_iter()
|
||||
.map(|task| {
|
||||
task::spawn(async move {
|
||||
task().await;
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
// simpler version for synchronous tasks
|
||||
pub async fn execute_parallel_sync<F>(tasks: Vec<F>)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let handles: Vec<_> = tasks
|
||||
.into_iter()
|
||||
.map(|task| {
|
||||
task::spawn_blocking(move || {
|
||||
task();
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
123
src/lexer/mod.rs
Normal file
123
src/lexer/mod.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Token {
|
||||
Identifier(String),
|
||||
Number(i64),
|
||||
Assign,
|
||||
Plus,
|
||||
Minus,
|
||||
Multiply,
|
||||
Divide,
|
||||
If,
|
||||
Else,
|
||||
While,
|
||||
Print,
|
||||
Eof,
|
||||
}
|
||||
|
||||
pub struct Lexer<'a> {
|
||||
input: &'a str,
|
||||
position: usize,
|
||||
current_char: Option<char>,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Lexer {
|
||||
input,
|
||||
position: 0,
|
||||
current_char: input.chars().next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.position += 1;
|
||||
self.current_char = self.input.chars().nth(self.position);
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_whitespace() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Token {
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_whitespace() {
|
||||
self.skip_whitespace();
|
||||
continue;
|
||||
}
|
||||
|
||||
if c.is_alphabetic() {
|
||||
let identifier = self.collect_identifier();
|
||||
return match identifier.as_str() {
|
||||
"if" => Token::If,
|
||||
"else" => Token::Else,
|
||||
"while" => Token::While,
|
||||
"print" => Token::Print,
|
||||
_ => Token::Identifier(identifier),
|
||||
};
|
||||
}
|
||||
|
||||
if c.is_digit(10) {
|
||||
return Token::Number(self.collect_number());
|
||||
}
|
||||
|
||||
if c == '=' {
|
||||
self.advance();
|
||||
return Token::Assign;
|
||||
}
|
||||
|
||||
if c == '+' {
|
||||
self.advance();
|
||||
return Token::Plus;
|
||||
}
|
||||
|
||||
if c == '-' {
|
||||
self.advance();
|
||||
return Token::Minus;
|
||||
}
|
||||
|
||||
if c == '*' {
|
||||
self.advance();
|
||||
return Token::Multiply;
|
||||
}
|
||||
|
||||
if c == '/' {
|
||||
self.advance();
|
||||
return Token::Divide;
|
||||
}
|
||||
|
||||
self.advance();
|
||||
}
|
||||
|
||||
Token::Eof
|
||||
}
|
||||
|
||||
fn collect_identifier(&mut self) -> String {
|
||||
let start_pos = self.position;
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_alphanumeric() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.input[start_pos..self.position].to_string()
|
||||
}
|
||||
|
||||
fn collect_number(&mut self) -> i64 {
|
||||
let start_pos = self.position;
|
||||
while let Some(c) = self.current_char {
|
||||
if c.is_digit(10) {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.input[start_pos..self.position].parse().unwrap()
|
||||
}
|
||||
}
|
||||
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod error;
|
||||
pub mod interpreter;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod runtime;
|
||||
20
src/main.rs
Normal file
20
src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: rush <script.rsh>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let script_path = &args[1];
|
||||
|
||||
let content = std::fs::read_to_string(script_path).expect("Failed to read the script file");
|
||||
|
||||
let stripped_content = if content.starts_with("#!") {
|
||||
content.lines().skip(1).collect::<Vec<&str>>().join("\n")
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
rush::interpreter::execute(&stripped_content);
|
||||
}
|
||||
60
src/parser/ast.rs
Normal file
60
src/parser/ast.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AstNode {
|
||||
VariableAssignment {
|
||||
name: String,
|
||||
value: Box<AstNode>,
|
||||
},
|
||||
ControlFlow {
|
||||
condition: Box<AstNode>,
|
||||
then_branch: Vec<AstNode>,
|
||||
else_branch: Option<Vec<AstNode>>,
|
||||
},
|
||||
Command {
|
||||
name: String,
|
||||
args: Vec<AstNode>,
|
||||
},
|
||||
For {
|
||||
var: String,
|
||||
items: Vec<String>,
|
||||
body: Vec<AstNode>,
|
||||
},
|
||||
Parallel {
|
||||
blocks: Vec<Vec<AstNode>>,
|
||||
},
|
||||
Workers {
|
||||
count: usize,
|
||||
body: Vec<AstNode>,
|
||||
},
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Program {
|
||||
pub statements: Vec<AstNode>,
|
||||
}
|
||||
|
||||
// types for executor compatibility
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Statement {
|
||||
Command(Command),
|
||||
Assignment(String, String),
|
||||
For {
|
||||
var: String,
|
||||
items: Vec<String>,
|
||||
body: Vec<Statement>,
|
||||
},
|
||||
If {
|
||||
condition: String,
|
||||
then_branch: Vec<Statement>,
|
||||
else_branch: Option<Vec<Statement>>,
|
||||
},
|
||||
Parallel {
|
||||
blocks: Vec<Vec<Statement>>,
|
||||
},
|
||||
}
|
||||
393
src/parser/mod.rs
Normal file
393
src/parser/mod.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
pub mod ast;
|
||||
|
||||
use crate::error::RushError;
|
||||
pub use ast::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct Parser {
|
||||
// todo: add fields for the parser state
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Self {
|
||||
Parser {
|
||||
// todo: init fields
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&self, input: &str) -> Result<Program, RushError> {
|
||||
let mut statements = Vec::new();
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < lines.len() {
|
||||
let (node, next_i) = self.parse_statement(&lines, i)?;
|
||||
if let Some(n) = node {
|
||||
statements.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
|
||||
let program = Program { statements };
|
||||
|
||||
// validate variables before returning
|
||||
self.validate_variables(&program)?;
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
fn validate_variables(&self, program: &Program) -> Result<(), RushError> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut defined_vars: HashSet<String> = HashSet::new();
|
||||
|
||||
for statement in &program.statements {
|
||||
self.validate_node(statement, &mut defined_vars)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_node(
|
||||
&self,
|
||||
node: &AstNode,
|
||||
defined_vars: &mut HashSet<String>,
|
||||
) -> Result<(), RushError> {
|
||||
match node {
|
||||
AstNode::VariableAssignment { name, value } => {
|
||||
// first check if the value uses any undefined variables
|
||||
self.check_node_for_undefined_vars(value, defined_vars)?;
|
||||
// then add this variable to the defined set
|
||||
defined_vars.insert(name.clone());
|
||||
}
|
||||
AstNode::Command { name: _, args } => {
|
||||
// check all arguments for variable references
|
||||
for arg in args {
|
||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||
}
|
||||
}
|
||||
AstNode::For {
|
||||
var,
|
||||
items: _,
|
||||
body,
|
||||
} => {
|
||||
// create a new scope for the for loop variable
|
||||
let mut loop_scope = defined_vars.clone();
|
||||
loop_scope.insert(var.clone());
|
||||
|
||||
// validate the body with the loop variable in scope
|
||||
for body_node in body {
|
||||
self.validate_node(body_node, &mut loop_scope)?;
|
||||
}
|
||||
}
|
||||
AstNode::Parallel { blocks } => {
|
||||
// each parallel block sees the current scope but can't define new vars
|
||||
for block in blocks {
|
||||
for block_node in block {
|
||||
let mut parallel_scope = defined_vars.clone();
|
||||
self.validate_node(block_node, &mut parallel_scope)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Workers { count: _, body } => {
|
||||
// workers blocks see the current scope
|
||||
for worker_node in body {
|
||||
let mut worker_scope = defined_vars.clone();
|
||||
self.validate_node(worker_node, &mut worker_scope)?;
|
||||
}
|
||||
}
|
||||
AstNode::ControlFlow {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} => {
|
||||
self.check_node_for_undefined_vars(condition, defined_vars)?;
|
||||
for node in then_branch {
|
||||
self.validate_node(node, defined_vars)?;
|
||||
}
|
||||
if let Some(else_nodes) = else_branch {
|
||||
for node in else_nodes {
|
||||
self.validate_node(node, defined_vars)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::Literal(_) => {
|
||||
// literals dont use vars
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_node_for_undefined_vars(
|
||||
&self,
|
||||
node: &AstNode,
|
||||
defined_vars: &HashSet<String>,
|
||||
) -> Result<(), RushError> {
|
||||
match node {
|
||||
AstNode::Literal(s) => {
|
||||
// check if this is a variable reference
|
||||
if s.starts_with('$') {
|
||||
let var_name = &s[1..];
|
||||
if !defined_vars.contains(var_name) {
|
||||
return Err(RushError::VariableError(format!(
|
||||
"Variable '{}' is used before being defined",
|
||||
var_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
AstNode::VariableAssignment { name: _, value } => {
|
||||
self.check_node_for_undefined_vars(value, defined_vars)?;
|
||||
}
|
||||
AstNode::Command { name: _, args } => {
|
||||
for arg in args {
|
||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// other node types dont directly contain variable references
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_statement(
|
||||
&self,
|
||||
lines: &[&str],
|
||||
mut i: usize,
|
||||
) -> Result<(Option<AstNode>, usize), RushError> {
|
||||
let line = lines[i].trim();
|
||||
|
||||
// skip empty lines and comments
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return Ok((None, i + 1));
|
||||
}
|
||||
|
||||
// parse variable assignment: name = "value"
|
||||
// make sure it's actually an assignment (not a command with = in args)
|
||||
if line.contains('=')
|
||||
&& !line.starts_with("if")
|
||||
&& !line.starts_with("for")
|
||||
&& !line.starts_with("echo")
|
||||
{
|
||||
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||
if parts.len() == 2 {
|
||||
let var_name = parts[0].trim();
|
||||
// only treat as assignment if var_name is a valid identifier (no spaces, no special chars)
|
||||
if !var_name.is_empty() && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
{
|
||||
let value = parts[1].trim().trim_matches('"').to_string();
|
||||
return Ok((
|
||||
Some(AstNode::VariableAssignment {
|
||||
name: var_name.to_string(),
|
||||
value: Box::new(AstNode::Literal(value)),
|
||||
}),
|
||||
i + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse for loop: for var in items { ... }
|
||||
if line.starts_with("for ") {
|
||||
// parse: for i in 1 2 3 {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 4 && parts[0] == "for" && parts[2] == "in" {
|
||||
let var_name = parts[1].to_string();
|
||||
|
||||
// collect items until we hit '{'
|
||||
let mut items = Vec::new();
|
||||
for part in &parts[3..] {
|
||||
if *part == "{" {
|
||||
break;
|
||||
}
|
||||
items.push(part.trim_matches('"').to_string());
|
||||
}
|
||||
|
||||
// parse body
|
||||
i += 1;
|
||||
let mut body = Vec::new();
|
||||
while i < lines.len() && !lines[i].trim().starts_with('}') {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
body.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
i += 1; // skip closing brace
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::For {
|
||||
var: var_name,
|
||||
items,
|
||||
body,
|
||||
}),
|
||||
i,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// parse parallel block: parallel { ... }
|
||||
if line.starts_with("parallel") && line.contains('{') {
|
||||
i += 1;
|
||||
let mut blocks = Vec::new();
|
||||
|
||||
while i < lines.len() {
|
||||
let inner_line = lines[i].trim();
|
||||
|
||||
// end of parallel block
|
||||
if inner_line.starts_with('}') {
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// handle 'run {' blocks inside parallel
|
||||
if inner_line.starts_with("run") && inner_line.contains('{') {
|
||||
let mut current_block = Vec::new();
|
||||
|
||||
// check if single line
|
||||
if inner_line.contains('}') {
|
||||
|
||||
let start = inner_line.find('{').unwrap() + 1;
|
||||
let end = inner_line.rfind('}').unwrap();
|
||||
let content = &inner_line[start..end].trim();
|
||||
|
||||
// parse commands separated by semicolons
|
||||
for cmd_part in content.split(';') {
|
||||
let cmd_trimmed = cmd_part.trim();
|
||||
if !cmd_trimmed.is_empty() {
|
||||
let parts: Vec<&str> = cmd_trimmed.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
AstNode::Literal(arg.trim_matches('"').to_string())
|
||||
})
|
||||
.collect();
|
||||
current_block.push(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks.push(current_block);
|
||||
i += 1;
|
||||
} else {
|
||||
// multi line run block
|
||||
i += 1;
|
||||
|
||||
while i < lines.len() {
|
||||
let run_line = lines[i].trim();
|
||||
|
||||
// end of this run block
|
||||
if run_line.starts_with('}') {
|
||||
blocks.push(current_block);
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// handle multiple commands on one line separated by semicolons
|
||||
if run_line.contains(';') {
|
||||
for cmd_part in run_line.split(';') {
|
||||
let cmd_trimmed = cmd_part.trim();
|
||||
if !cmd_trimmed.is_empty() {
|
||||
let parts: Vec<&str> =
|
||||
cmd_trimmed.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
AstNode::Literal(
|
||||
arg.trim_matches('"').to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
current_block.push(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
} else {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
current_block.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// direct statement in parallel block (not inside a run {})
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
blocks.push(vec![n]);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok((Some(AstNode::Parallel { blocks }), i));
|
||||
}
|
||||
|
||||
// parse workers N {} block
|
||||
if line.starts_with("workers ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let worker_count = parts[1].parse::<usize>().unwrap_or(1);
|
||||
|
||||
i += 1;
|
||||
let mut body = Vec::new();
|
||||
while i < lines.len() && !lines[i].trim().starts_with('}') {
|
||||
let (node, next_i) = self.parse_statement(lines, i)?;
|
||||
if let Some(n) = node {
|
||||
body.push(n);
|
||||
}
|
||||
i = next_i;
|
||||
}
|
||||
i += 1; // skip closing brace
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::Workers {
|
||||
count: worker_count,
|
||||
body,
|
||||
}),
|
||||
i,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// parse command: echo "text" or any other command
|
||||
if !line.is_empty() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
let cmd_name = parts[0].to_string();
|
||||
let args: Vec<AstNode> = parts[1..]
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
// kep $ for variable references, remove quotes
|
||||
let cleaned = arg.trim_matches('"');
|
||||
AstNode::Literal(cleaned.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok((
|
||||
Some(AstNode::Command {
|
||||
name: cmd_name,
|
||||
args,
|
||||
}),
|
||||
i + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((None, i + 1))
|
||||
}
|
||||
}
|
||||
|
||||
//todo: add additional functions and implementations for the parser
|
||||
31
src/runtime/builtins.rs
Normal file
31
src/runtime/builtins.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub fn echo(args: Vec<String>) {
|
||||
println!("{}", args.join(" "));
|
||||
}
|
||||
|
||||
pub fn require_root() {
|
||||
if !is_root() {
|
||||
eprintln!("This command requires root privileges.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(code: i32) {
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
fn is_root() -> bool {
|
||||
// check if the current user is root by checking UID
|
||||
// on Unix systems, root has UID 0
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// try to get process UID using USER env variable
|
||||
std::env::var("USER").map(|u| u == "root").unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
//todo
|
||||
// On non-Unix systems, assume not root
|
||||
false
|
||||
}
|
||||
}
|
||||
26
src/runtime/environment.rs
Normal file
26
src/runtime/environment.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn new() -> Self {
|
||||
Environment {
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_variable(&mut self, name: String, value: String) {
|
||||
self.variables.insert(name, value);
|
||||
}
|
||||
|
||||
pub fn get_variable(&self, name: &str) -> Option<&String> {
|
||||
self.variables.get(name)
|
||||
}
|
||||
|
||||
pub fn remove_variable(&mut self, name: &str) {
|
||||
self.variables.remove(name);
|
||||
}
|
||||
}
|
||||
2
src/runtime/mod.rs
Normal file
2
src/runtime/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod builtins;
|
||||
pub mod environment;
|
||||
254
tests/interpreter_tests.rs
Normal file
254
tests/interpreter_tests.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use rush::interpreter::executor::Executor;
|
||||
use rush::parser::Parser;
|
||||
use rush::runtime::environment::Environment;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_variable_assignment() {
|
||||
// Test variable assignment functionality
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse("name = \"test\"");
|
||||
assert!(result.is_ok());
|
||||
|
||||
let program = result.unwrap();
|
||||
let mut env = Environment::new();
|
||||
let mut executor = Executor::new(&mut env);
|
||||
|
||||
for stmt in program.statements {
|
||||
executor.execute_node(stmt);
|
||||
}
|
||||
|
||||
assert_eq!(env.get_variable("name"), Some(&"test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_command() {
|
||||
// Test basic command execution doesn't panic
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse("echo \"test\"");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_parsing() {
|
||||
// Test the parsing of a complete .rsh script
|
||||
let script = r#"
|
||||
name = "Louis"
|
||||
echo "Hello" $name
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert!(program.statements.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undefined_variable_detection() {
|
||||
// Test that using an undefined variable is caught
|
||||
let script = r#"
|
||||
echo "Hello" $undefined_var
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("undefined_var"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_order_variable_detection() {
|
||||
// Test that using a variable before it's defined is caught
|
||||
let script = r#"
|
||||
echo "Hello" $name
|
||||
name = "Louis"
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_variable_order() {
|
||||
// Test that defining before using works correctly
|
||||
let script = r#"
|
||||
name = "Louis"
|
||||
echo "Hello" $name
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_variable_scope() {
|
||||
// Test that for loop variables are properly scoped
|
||||
let script = r#"
|
||||
for i in 1 2 3 {
|
||||
echo "Number:" $i
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_variable_isolation() {
|
||||
// Test that for loop variables don't leak to outer scope
|
||||
let script = r#"
|
||||
for i in 1 2 3 {
|
||||
echo "Inside:" $i
|
||||
}
|
||||
echo "Outside:" $i
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_block_variable_access() {
|
||||
// Test that parallel blocks can access outer scope variables
|
||||
let script = r#"
|
||||
name = "Test"
|
||||
|
||||
parallel {
|
||||
run { echo "Task 1:" $name }
|
||||
run { echo "Task 2:" $name }
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_block_undefined_variable() {
|
||||
// Test that parallel blocks catch undefined variables
|
||||
let script = r#"
|
||||
parallel {
|
||||
run { echo "Task:" $undefined }
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workers_block_variable_access() {
|
||||
// Test that workers blocks can access outer scope variables
|
||||
let script = r#"
|
||||
msg = "Worker"
|
||||
|
||||
workers 2 {
|
||||
echo $msg "Task 1"
|
||||
echo $msg "Task 2"
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workers_with_for_loop() {
|
||||
// Test that workers can contain for loops with variables
|
||||
let script = r#"
|
||||
prefix = "Item"
|
||||
|
||||
workers 2 {
|
||||
for num in 1 2 3 {
|
||||
echo $prefix $num
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_for_loops() {
|
||||
// Test nested for loops with proper scoping
|
||||
let script = r#"
|
||||
for i in 1 2 {
|
||||
for j in a b {
|
||||
echo $i $j
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_variables() {
|
||||
// Test multiple variable definitions and usage
|
||||
let script = r#"
|
||||
user = "Alice"
|
||||
project = "RushShell"
|
||||
version = "1.0"
|
||||
|
||||
echo $user "is working on" $project "version" $version
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_with_outer_variables() {
|
||||
// Test for loops can access both loop vars and outer vars
|
||||
let script = r#"
|
||||
prefix = "Item"
|
||||
|
||||
for num in 1 2 3 {
|
||||
echo $prefix $num
|
||||
}
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comprehensive_variable_usage() {
|
||||
// Test a comprehensive script with all variable features
|
||||
let script = r#"
|
||||
user = "Alice"
|
||||
project = "RushShell"
|
||||
|
||||
echo "Welcome" $user "to" $project
|
||||
|
||||
for item in "docs" "tests" "src" {
|
||||
echo $user "is checking" $item
|
||||
}
|
||||
|
||||
parallel {
|
||||
run { echo "Parallel 1:" $user }
|
||||
run { echo "Parallel 2:" $project }
|
||||
}
|
||||
|
||||
workers 2 {
|
||||
for num in 1 2 {
|
||||
echo $project "worker task" $num
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done with" $project
|
||||
"#;
|
||||
let parser = Parser::new();
|
||||
let result = parser.parse(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
42
tests/lexer_tests.rs
Normal file
42
tests/lexer_tests.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use rush::lexer::{Lexer, Token};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenization() {
|
||||
let mut lexer = Lexer::new("x = 10");
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("x".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Assign);
|
||||
assert_eq!(lexer.next_token(), Token::Number(10));
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identifier_tokens() {
|
||||
let mut lexer = Lexer::new("foo bar");
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("foo".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Identifier("bar".to_string()));
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_control_flow_tokens() {
|
||||
let mut lexer = Lexer::new("if else while");
|
||||
assert_eq!(lexer.next_token(), Token::If);
|
||||
assert_eq!(lexer.next_token(), Token::Else);
|
||||
assert_eq!(lexer.next_token(), Token::While);
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operators() {
|
||||
let mut lexer = Lexer::new("+ - * /");
|
||||
assert_eq!(lexer.next_token(), Token::Plus);
|
||||
assert_eq!(lexer.next_token(), Token::Minus);
|
||||
assert_eq!(lexer.next_token(), Token::Multiply);
|
||||
assert_eq!(lexer.next_token(), Token::Divide);
|
||||
assert_eq!(lexer.next_token(), Token::Eof);
|
||||
}
|
||||
}
|
||||
64
tests/parser_tests.rs
Normal file
64
tests/parser_tests.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use rush::parser::{AstNode, Parser};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_variable_assignment() {
|
||||
// Test parsing a variable assignment
|
||||
let parser = Parser::new();
|
||||
let input = "x = \"10\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
|
||||
match &program.statements[0] {
|
||||
AstNode::VariableAssignment { name, .. } => {
|
||||
assert_eq!(name, "x");
|
||||
}
|
||||
_ => panic!("Expected variable assignment"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_parsing() {
|
||||
// Test parsing a simple command
|
||||
let parser = Parser::new();
|
||||
let input = "echo \"Hello World\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
|
||||
match &program.statements[0] {
|
||||
AstNode::Command { name, .. } => {
|
||||
assert_eq!(name, "echo");
|
||||
}
|
||||
_ => panic!("Expected command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_statements() {
|
||||
// Test parsing multiple statements
|
||||
let parser = Parser::new();
|
||||
let input = "name = \"Louis\"\necho \"Hello\" $name";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_comments() {
|
||||
// Test that comments are skipped
|
||||
let parser = Parser::new();
|
||||
let input = "# This is a comment\necho \"test\"";
|
||||
let result = parser.parse(input);
|
||||
assert!(result.is_ok());
|
||||
let program = result.unwrap();
|
||||
assert_eq!(program.statements.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user