use std::path::PathBuf;
use std::process::Command;
const PROGRAMS: &[&str] = &[
"set x 5\nputs $x",
"set x 5\nset y $x\nputs \"$x$y\"",
"set x 5\nputs [set x]",
"set greeting hello\nputs \"$greeting, world\"",
"puts [set a 3]",
"set x 05\nputs $x",
"set x 1.10\nputs $x",
"set x { spaced }\nputs \"<$x>\"",
"puts [expr {1+2*3}]",
"puts [expr {(1+2)*3}]",
"puts [expr {-57 / 10}]",
"puts [expr {-57 % 10}]",
"puts [expr {57 / -10}]",
"puts [expr {57 % -10}]",
"puts [expr {7/2}]",
"puts [expr {2**10}]",
"puts [expr {2**3**2}]",
"puts [expr {3.0/2}]",
"puts [expr {1.0/3}]",
"puts [expr {1.0+1}]",
"puts [expr {2.0*3}]",
"puts [expr {0.1+0.2}]",
"puts [expr {1e300*10}]",
"puts [expr {1.0e-7/10}]",
"puts [expr {2**0.5}]",
"puts [expr {10 < 9}]",
"puts [expr {\"10\" < \"9\"}]",
"puts [expr {\"abc\" < \"abd\"}]",
"puts [expr {1 == 1.0}]",
"puts [expr {\"a\" eq \"a\"}]",
"puts [expr {\"a\" ne \"b\"}]",
"puts [expr {\"abc\" lt \"abd\"}]",
"puts [expr {\"a\" eq \"a\" == 1}]",
"puts [expr {1 && 0}]",
"puts [expr {0 || 3}]",
"puts [expr {!5}]",
"puts [expr {~5}]",
"puts [expr {-8 >> 1}]",
"puts [expr {1 << 3}]",
"puts [expr {6 & 3}]",
"puts [expr {6 | 3}]",
"puts [expr {6 ^ 3}]",
"puts [expr {1 ? 2 : 3}]",
"puts [expr {0 ? 2 : 3}]",
"set a 4\nset b 6\nputs [expr {$a*$b}]",
"set a 4\nputs [expr {[expr {$a+1}] * 2}]",
"set s abc\nputs [expr {$s eq \"abc\"}]",
"set x 10\nputs [expr {$x > 3 && $x < 20}]",
"puts [expr {0xff + 1}]",
"puts [expr {0b1010}]",
"puts [expr {0o17}]",
"if {1} {puts yes}",
"if {0} {puts yes} else {puts no}",
"if {0} {puts a} elseif {1} {puts b} else {puts c}",
"set x 3\nif {$x > 2} {puts big} else {puts small}",
"puts [if {1} {expr 41+1}]",
"set i 0\nwhile {$i < 3} {puts $i; incr i}",
"set i 0\nwhile {$i < 5} {incr i; if {$i == 3} {break}}\nputs $i",
"set i 0\nset n 0\nwhile {$i < 5} {incr i; if {$i == 3} {continue}; incr n}\nputs $n",
"set i 10\nwhile {0} {puts never}\nputs $i",
"set total 0\nset i 1\nwhile {$i <= 100} {set total [expr {$total + $i}]; incr i}\nputs $total",
"for {set i 0} {$i < 3} {incr i} {puts $i}",
"for {set i 0} {0} {incr i} {puts never}\nputs done",
"for {set i 0} {$i < 9} {incr i} {if {$i == 4} {break}}\nputs $i",
"for {set i 0} {$i < 5} {incr i} {if {$i == 2} {continue}; puts $i}",
"set n 0\nfor {set i 0} {$i < 4} {incr i} {continue; incr n}\nputs \"$i $n\"",
"puts [for {set i 0} {$i < 2} {incr i} {set x $i}]",
"foreach x {a b c} {puts $x}",
"foreach x {} {puts never}\nputs done",
"foreach x {a b c d} {if {$x eq \"c\"} {break}; puts $x}",
"foreach x {a b c d} {if {$x eq \"b\"} {continue}; puts $x}",
"foreach {a b} {1 2 3} {puts \"$a|$b\"}",
"foreach a {1 2} b {x y} {puts \"$a$b\"}",
"puts [foreach x {a b} {set y $x}]",
"for {set i 0} {$i < 3} {incr i} {for {set j 0} {$j < 3} {incr j} {if {$j == 1} {break}; puts \"$i$j\"}}",
"set i 0\nwhile {$i < 3} {incr i; foreach x {a b} {if {$x eq \"b\"} {continue}; puts \"$i$x\"}}",
"foreach x {1 2 3} {set j 0\nwhile {$j < $x} {incr j}\nputs \"$x:$j\"}",
"set i 0\nwhile {$i < 4} {incr i; if {$i == 2} {continue}; if {$i == 3} {break}; puts $i}\nputs $i",
"set i 5\nputs [incr i]",
"set i 5\nputs [incr i 3]",
"set i 5\nincr i -2\nputs $i",
"puts -nonewline a\nputs b",
"puts {}",
"puts \"\"",
"# leading comment\nputs a ;# trailing\nputs b",
"puts a; puts b",
];
fn tclsh() -> Option<PathBuf> {
for name in ["tclsh", "tclsh9.0", "tclsh8.6"] {
if let Ok(out) = Command::new("sh")
.arg("-c")
.arg(format!("command -v {name}"))
.output()
{
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
}
None
}
fn reference_output(tclsh: &PathBuf, program: &str) -> String {
let path = std::env::temp_dir().join(format!("tclrs-exec-{}.tcl", std::process::id()));
std::fs::write(&path, program).expect("write program");
let out = Command::new(tclsh).arg(&path).output().expect("run tclsh");
let _ = std::fs::remove_file(&path);
assert!(
out.status.success(),
"tclsh rejected program:\n{program}\n{}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn execution_matches_tclsh() {
let Some(tclsh) = tclsh() else {
eprintln!("skipping: no tclsh on PATH");
return;
};
let mut failures = Vec::new();
for program in PROGRAMS {
let expected = reference_output(&tclsh, program);
match tclrs::eval(program) {
Ok(outcome) if outcome.output == expected => {}
Ok(outcome) => failures.push(format!(
"program:\n{program}\n tclsh: {expected:?}\n tclrs: {:?}",
outcome.output
)),
Err(e) => failures.push(format!(
"program:\n{program}\n tclsh: {expected:?}\n tclrs failed: {e}"
)),
}
}
assert!(
failures.is_empty(),
"{} of {} programs diverge:\n\n{}",
failures.len(),
PROGRAMS.len(),
failures.join("\n\n")
);
}
#[test]
fn script_value_is_the_last_command() {
assert_eq!(tclrs::eval("set x 7").unwrap().result, "7");
assert_eq!(tclrs::eval("set x 7\nexpr {$x*2}").unwrap().result, "14");
assert_eq!(tclrs::eval("").unwrap().result, "");
assert_eq!(tclrs::eval("while {0} {puts x}").unwrap().result, "");
}
#[test]
fn unsupported_constructs_are_refused() {
for (src, expected) in [
("uplevel 1 {set x 1}", "invalid command name \"uplevel\""),
(
"array startsearch a",
"array startsearch is not supported yet",
),
(
"return 1",
"\"return\" outside of a procedure is not supported",
),
(
"puts [expr {sin(1)}]",
"math function \"sin\" is not supported yet",
),
("break", "invoked \"break\" outside of a loop"),
] {
let err = tclrs::eval(src).expect_err(&format!("{src:?} should fail"));
assert!(
err.contains(expected),
"{src:?}: expected an error mentioning {expected:?}, got {err:?}"
);
}
}
#[test]
fn integer_overflow_is_an_error_not_a_wrap() {
let err = tclrs::eval("puts [expr {9223372036854775807 + 1}]").expect_err("should overflow");
assert!(err.contains("too large"), "got {err:?}");
}
#[test]
fn min_int_over_negative_one_does_not_trap() {
let min = "set min [expr {-9223372036854775807 - 1}]\n";
let outcome = tclrs::eval(&format!("{min}puts [expr {{$min % -1}}]")).expect("remainder");
assert_eq!(outcome.output, "0\n", "tclsh prints 0 for this remainder");
let err = tclrs::eval(&format!("{min}puts [expr {{$min / -1}}]"))
.expect_err("the quotient does not fit in i64");
assert!(err.contains("too large"), "got {err:?}");
}