initial commit

This commit is contained in:
2025-11-01 22:45:55 +01:00
commit 78959d4f22
19 changed files with 2027 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

534
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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);
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}

View 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
View File

@@ -0,0 +1,2 @@
pub mod builtins;
pub mod environment;

254
tests/interpreter_tests.rs Normal file
View 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
View 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
View 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);
}
}