use crate::formal::semantics::{posix_semantics, rash_semantics};
use crate::formal::{AbstractState, FormalEmitter, TinyAst};
use proptest::prelude::*;
pub fn arb_tiny_ast() -> impl Strategy<Value = TinyAst> {
let leaf = prop_oneof![arb_execute_command(), arb_set_env(), arb_change_dir(),];
leaf.prop_recursive(
8, 256, 10, |inner| {
prop::collection::vec(inner, 1..=5).prop_map(|commands| TinyAst::Sequence { commands })
},
)
}
fn arb_execute_command() -> impl Strategy<Value = TinyAst> {
let commands = prop::sample::select(vec![
"echo", "mkdir", "test", "cp", "mv", "rm", "chmod", "chown",
]);
let args = prop::collection::vec(arb_safe_string(), 0..=3);
(commands, args).prop_map(|(command_name, args)| TinyAst::ExecuteCommand {
command_name: command_name.to_string(),
args,
})
}
fn arb_set_env() -> impl Strategy<Value = TinyAst> {
(arb_var_name(), arb_safe_string())
.prop_map(|(name, value)| TinyAst::SetEnvironmentVariable { name, value })
}
fn arb_change_dir() -> impl Strategy<Value = TinyAst> {
arb_path().prop_map(|path| TinyAst::ChangeDirectory { path })
}
fn arb_var_name() -> impl Strategy<Value = String> {
"[A-Z_][A-Z0-9_]{0,15}".prop_map(|s| s.to_string())
}
fn arb_safe_string() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_/.-]{0,20}".prop_map(|s| s.to_string())
}
fn arb_path() -> impl Strategy<Value = String> {
prop_oneof![
Just("/".to_string()),
Just("/tmp".to_string()),
Just("/opt".to_string()),
Just("/home".to_string()),
"[a-z]{1,8}".prop_map(|s| format!("/tmp/{s}")),
"[a-z]{1,8}".prop_map(|s| format!("/opt/{s}")),
]
}
#[cfg(test)]
proptest! {
#[test]
fn prop_semantic_equivalence(ast in arb_tiny_ast()) {
prop_assume!(ast.is_valid());
let initial_state = create_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);
match (rash_result, posix_result) {
(Ok(rash_state), Ok(posix_state)) => {
prop_assert!(
rash_state.is_equivalent(&posix_state),
"States not equivalent for AST: {:?}\nEmitted: {}\nRash: {:?}\nPOSIX: {:?}",
ast, posix_code, rash_state, posix_state
);
}
(Err(_), Err(_)) => {
}
(Ok(_rash_state), Err(posix_err)) => {
prop_assert!(
false,
"Rash succeeded but POSIX failed for AST: {:?}\nEmitted: {}\nError: {}",
ast, posix_code, posix_err
);
}
(Err(rash_err), Ok(_)) => {
prop_assert!(
false,
"Rash failed but POSIX succeeded for AST: {:?}\nError: {}",
ast, rash_err
);
}
}
}
#[test]
fn prop_emitter_produces_valid_posix(ast in arb_tiny_ast()) {
prop_assume!(ast.is_valid());
let posix_code = FormalEmitter::emit(&ast);
prop_assert!(!posix_code.is_empty());
let initial_state = create_test_state();
let parse_result = posix_semantics::eval_posix(&posix_code, initial_state);
let _ = parse_result;
}
#[test]
fn prop_echo_preserves_output(args in prop::collection::vec(arb_safe_string(), 0..=5)) {
let ast = TinyAst::ExecuteCommand {
command_name: "echo".to_string(),
args: args.clone(),
};
let initial_state = AbstractState::new();
let rash_state = rash_semantics::eval_rash(&ast, initial_state.clone()).unwrap();
let posix_code = FormalEmitter::emit(&ast);
let posix_state = posix_semantics::eval_posix(&posix_code, initial_state).unwrap();
prop_assert_eq!(rash_state.stdout, posix_state.stdout);
}
#[test]
fn prop_assignment_preserves_env(name in arb_var_name(), value in arb_safe_string()) {
let ast = TinyAst::SetEnvironmentVariable {
name: name.clone(),
value: value.clone(),
};
let initial_state = AbstractState::new();
let rash_state = rash_semantics::eval_rash(&ast, initial_state.clone()).unwrap();
let posix_code = FormalEmitter::emit(&ast);
let posix_state = posix_semantics::eval_posix(&posix_code, initial_state).unwrap();
prop_assert_eq!(rash_state.get_env(&name), posix_state.get_env(&name));
prop_assert_eq!(rash_state.get_env(&name), Some(&value));
}
}
fn create_test_state() -> AbstractState {
let mut state = AbstractState::new();
state.filesystem.insert(
std::path::PathBuf::from("/tmp"),
crate::formal::FileSystemEntry::Directory,
);
state.filesystem.insert(
std::path::PathBuf::from("/opt"),
crate::formal::FileSystemEntry::Directory,
);
state.filesystem.insert(
std::path::PathBuf::from("/home"),
crate::formal::FileSystemEntry::Directory,
);
state.set_env("PATH".to_string(), "/usr/bin:/bin".to_string());
state.set_env("HOME".to_string(), "/home/user".to_string());
state
}
pub struct FormalTheorem;
impl FormalTheorem {
pub const THEOREM: &'static str = r#"
Theorem semantic_equivalence:
forall (ast : TinyAst) (s : AbstractState),
is_valid ast = true ->
eval_rash ast s = eval_posix (emit ast) s.
Proof.
intros ast s H_valid.
induction ast; simpl in *.
- (* ExecuteCommand case *)
unfold eval_rash, eval_posix, emit.
reflexivity.
- (* SetEnvironmentVariable case *)
unfold eval_rash, eval_posix, emit.
reflexivity.
- (* Sequence case *)
induction commands.
+ (* Empty sequence *)
simpl. reflexivity.
+ (* Non-empty sequence *)
simpl. rewrite IHcommands. reflexivity.
- (* ChangeDirectory case *)
unfold eval_rash, eval_posix, emit.
reflexivity.
Qed.
"#;
pub const PROOF_SKETCH: &'static str = r#"
The proof proceeds by structural induction on the AST:
1. Base cases (ExecuteCommand, SetEnvironmentVariable, ChangeDirectory):
- Show that emit produces POSIX code with identical semantics
- The key is that our emit function preserves the exact behavior
2. Inductive case (Sequence):
- Use the induction hypothesis on each command in the sequence
- Show that sequential composition is preserved by emit
The proof relies on:
- Correct implementation of eval_rash and eval_posix
- Correct quoting/escaping in the emit function
- The restricted nature of our tiny AST subset
"#;
}
#[cfg(test)]
mod formal_tests {
use super::*;
#[test]
fn test_theorem_documentation() {
assert!(FormalTheorem::THEOREM.len() > 100);
assert!(FormalTheorem::PROOF_SKETCH.len() > 100);
}
}