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