use crate::formal::TinyAst;
pub struct FormalEmitter;
impl FormalEmitter {
pub fn emit(ast: &TinyAst) -> String {
match ast {
TinyAst::ExecuteCommand { command_name, args } => {
Self::emit_command(command_name, args)
}
TinyAst::SetEnvironmentVariable { name, value } => Self::emit_assignment(name, value),
TinyAst::Sequence { commands } => Self::emit_sequence(commands),
TinyAst::ChangeDirectory { path } => Self::emit_cd(path),
}
}
fn emit_command(name: &str, args: &[String]) -> String {
let mut parts = vec![name.to_string()];
for arg in args {
parts.push(Self::quote_argument(arg));
}
parts.join(" ")
}
fn emit_assignment(name: &str, value: &str) -> String {
format!("{}={}", name, Self::quote_value(value))
}
fn emit_sequence(commands: &[TinyAst]) -> String {
commands
.iter()
.map(Self::emit)
.collect::<Vec<_>>()
.join("; ")
}
fn emit_cd(path: &str) -> String {
format!("cd {}", Self::quote_argument(path))
}
fn quote_argument(arg: &str) -> String {
if arg.is_empty()
|| arg.contains(|c: char| {
c.is_whitespace()
|| matches!(
c,
'$' | '`'
| '"'
| '\''
| '\\'
| '!'
| '#'
| '&'
| '*'
| '('
| ')'
| ';'
| '<'
| '>'
| '?'
| '['
| ']'
| '{'
| '}'
| '|'
| '~'
)
})
{
format!("\"{}\"", Self::escape_for_double_quotes(arg))
} else {
arg.to_string()
}
}
fn quote_value(value: &str) -> String {
format!("\"{}\"", Self::escape_for_double_quotes(value))
}
fn escape_for_double_quotes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'$' => result.push_str("\\$"),
'`' => result.push_str("\\`"),
'\n' => result.push_str("\\n"),
_ => result.push(ch),
}
}
result
}
}
pub fn verify_semantic_equivalence(ast: &TinyAst) -> Result<(), String> {
use crate::formal::semantics::{posix_semantics, rash_semantics};
use crate::formal::AbstractState;
let initial_state = AbstractState::test_state();
let rash_result = rash_semantics::eval_rash(ast, initial_state.clone())?;
let posix_code = FormalEmitter::emit(ast);
let posix_result = posix_semantics::eval_posix(&posix_code, initial_state)?;
if rash_result.is_equivalent(&posix_result) {
Ok(())
} else {
Err(format!(
"Semantic equivalence failed for AST: {ast:?}\nEmitted: {posix_code}\nRash state: {rash_result:?}\nPOSIX state: {posix_result:?}"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emit_simple_command() {
let ast = TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: vec!["Hello World".to_string()],
};
let emitted = FormalEmitter::emit(&ast);
assert_eq!(emitted, "echo \"Hello World\"");
}
#[test]
fn test_emit_assignment() {
let ast = TinyAst::SetEnvironmentVariable {
name: "PATH".to_string(),
value: "/usr/bin:/bin".to_string(),
};
let emitted = FormalEmitter::emit(&ast);
assert_eq!(emitted, "PATH=\"/usr/bin:/bin\"");
}
#[test]
fn test_emit_sequence() {
let ast = TinyAst::Sequence {
commands: vec![
TinyAst::SetEnvironmentVariable {
name: "DIR".to_string(),
value: "/opt/rash".to_string(),
},
TinyAst::ExecuteCommand {
command_name: "mkdir".to_string(),
args: vec!["-p".to_string(), "/opt/rash".to_string()],
},
TinyAst::ChangeDirectory {
path: "/opt/rash".to_string(),
},
],
};
let emitted = FormalEmitter::emit(&ast);
assert_eq!(
emitted,
"DIR=\"/opt/rash\"; mkdir -p /opt/rash; cd /opt/rash"
);
}
#[test]
fn test_quote_special_characters() {
let ast = TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: vec!["$HOME/path with spaces".to_string()],
};
let emitted = FormalEmitter::emit(&ast);
assert_eq!(emitted, "echo \"\\$HOME/path with spaces\"");
}
#[test]
fn test_semantic_equivalence_echo() {
let ast = TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: vec!["Test".to_string()],
};
assert!(verify_semantic_equivalence(&ast).is_ok());
}
#[test]
fn test_semantic_equivalence_assignment() {
let ast = TinyAst::SetEnvironmentVariable {
name: "TEST_VAR".to_string(),
value: "test_value".to_string(),
};
assert!(verify_semantic_equivalence(&ast).is_ok());
}
#[test]
fn test_semantic_equivalence_sequence() {
let ast = TinyAst::Sequence {
commands: vec![
TinyAst::SetEnvironmentVariable {
name: "INSTALL_DIR".to_string(),
value: "/opt/rash".to_string(),
},
TinyAst::ExecuteCommand {
command_name: "mkdir".to_string(),
args: vec!["-p".to_string(), "/opt/rash/bin".to_string()],
},
],
};
assert!(verify_semantic_equivalence(&ast).is_ok());
}
}