use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::io::Write;
use std::sync::{Arc, Mutex};
use fusevm::{Chunk, Frame, NumOp, VMResult, Value, VM};
use crate::cache::ChunkCache;
use crate::compiler::{ext, ext_wide, Place};
use crate::coro::{self, Request};
use crate::list;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub result: String,
pub output: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TclError {
pub msg: String,
pub line: Option<usize>,
}
impl TclError {
pub(crate) fn plain(msg: impl Into<String>) -> Self {
TclError {
msg: msg.into(),
line: None,
}
}
}
impl fmt::Display for TclError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.line {
Some(line) => write!(f, "{} (line {line})", self.msg),
None => f.write_str(&self.msg),
}
}
}
impl std::error::Error for TclError {}
pub fn compile(src: &str) -> Result<Chunk, String> {
let rewritten = crate::rust_ffi::desugar(src);
let script = crate::parser::parse(&rewritten).map_err(|e| e.to_string())?;
crate::compiler::compile(&script).map_err(|e| e.to_string())
}
pub fn eval(src: &str) -> Result<Outcome, String> {
let (result, output) = eval_captured(src);
result.map(|result| Outcome { result, output })
}
pub fn eval_captured(src: &str) -> (Result<String, String>, String) {
let mut interp = Interp::capturing();
let result = interp.eval(src).map_err(|e| e.to_string());
(result, interp.take_output())
}
pub const DEFAULT_RECURSION_LIMIT: usize = 1000;
pub const RECOMMENDED_STACK: usize = 256 * 1024 * 1024;
#[derive(Clone)]
enum Output {
Capture(Arc<Mutex<String>>),
Stdout(Arc<Mutex<std::io::BufWriter<std::io::Stdout>>>),
}
impl Output {
fn stdout() -> Output {
Output::Stdout(Arc::new(Mutex::new(std::io::BufWriter::new(
std::io::stdout(),
))))
}
fn write(&self, s: &str) {
match self {
Output::Capture(buf) => buf.lock().expect("output lock").push_str(s),
Output::Stdout(out) => {
let _ = out.lock().expect("output lock").write_all(s.as_bytes());
}
}
}
fn flush(&self) {
if let Output::Stdout(out) = self {
let _ = out.lock().expect("output lock").flush();
}
}
}
struct State {
globals: HashMap<String, Value>,
cache: ChunkCache,
output: Output,
depth: usize,
limit: usize,
}
type Shared = Arc<Mutex<State>>;
pub struct Interp {
shared: Shared,
}
impl Interp {
pub fn new() -> Self {
Interp::with_output(Output::stdout())
}
pub fn capturing() -> Self {
Interp::with_output(Output::Capture(Arc::new(Mutex::new(String::new()))))
}
fn with_output(output: Output) -> Self {
Interp {
shared: Arc::new(Mutex::new(State {
globals: HashMap::new(),
cache: ChunkCache::new(),
output,
depth: 0,
limit: DEFAULT_RECURSION_LIMIT,
})),
}
}
pub fn set_recursion_limit(&mut self, limit: usize) {
self.lock().limit = limit.max(1);
}
pub fn eval(&mut self, src: &str) -> Result<String, TclError> {
run_source(&self.shared, src).map(|v| to_tcl_string(&v))
}
pub fn run_chunk(&mut self, chunk: fusevm::Chunk) -> Result<String, TclError> {
Machine::run(&self.shared, chunk).map(|v| to_tcl_string(&v))
}
pub fn set_global(&mut self, name: &str, value: impl Into<String>) {
let value = Value::Str(Arc::new(value.into()));
self.lock().globals.insert(name.to_string(), value);
}
pub fn global(&self, name: &str) -> Option<String> {
self.lock().globals.get(name).map(to_tcl_string)
}
pub fn global_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.lock().globals.keys().cloned().collect();
names.sort();
names
}
pub fn take_output(&mut self) -> String {
match &self.lock().output {
Output::Capture(buf) => std::mem::take(&mut buf.lock().expect("output lock")),
Output::Stdout(_) => String::new(),
}
}
pub fn cache_stats(&self) -> (u64, u64) {
self.lock().cache.stats()
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.shared.lock().expect("interpreter lock")
}
}
impl Default for Interp {
fn default() -> Self {
Interp::new()
}
}
fn run_source(shared: &Shared, src: &str) -> Result<Value, TclError> {
let compiled = {
let mut state = shared.lock().expect("interpreter lock");
if state.depth > state.limit {
return Err(TclError::plain(
"too many nested evaluations (infinite loop?)",
));
}
state.depth += 1;
state.cache.compile(src)
};
let result = compiled.and_then(|chunk| {
Machine::run(shared, (*chunk).clone())
});
shared.lock().expect("interpreter lock").depth -= 1;
result
}
fn seed(chunk: &Chunk, shared: &Shared) -> Vec<Value> {
let state = shared.lock().expect("interpreter lock");
chunk
.names
.iter()
.map(|name| state.globals.get(name).cloned().unwrap_or(Value::Undef))
.collect()
}
fn flush(chunk: &Chunk, shared: &Shared, globals: &[Value]) {
let mut state = shared.lock().expect("interpreter lock");
for (slot, name) in chunk.names.iter().enumerate() {
if name.starts_with('\u{0}') {
continue;
}
match globals.get(slot) {
Some(Value::Undef) | None => {
state.globals.remove(name);
}
Some(value) => {
state.globals.insert(name.clone(), value.clone());
}
}
}
}
struct CatchFrame {
handler: usize,
stack: usize,
frames: usize,
}
pub fn install_hooks(vm: &mut VM) -> Hooks {
let hooks = Hooks::new(Interp::new().shared);
hooks.install(vm);
hooks
}
pub fn install_hooks_capturing(vm: &mut VM, buf: Arc<Mutex<String>>) -> Hooks {
let hooks = Hooks::new(Interp::with_output(Output::Capture(buf)).shared);
hooks.install(vm);
hooks
}
pub struct Hooks {
output: Output,
error: Arc<Mutex<Option<TclError>>>,
catches: Arc<Mutex<Vec<CatchFrame>>>,
pending: Arc<Mutex<Option<Request>>>,
current: Arc<Mutex<Option<String>>>,
interp: Shared,
}
impl Hooks {
fn new(interp: Shared) -> Hooks {
let output = interp.lock().expect("interpreter lock").output.clone();
Hooks {
output,
error: Arc::new(Mutex::new(None)),
catches: Arc::new(Mutex::new(Vec::new())),
pending: Arc::new(Mutex::new(None)),
current: Arc::new(Mutex::new(None)),
interp,
}
}
pub fn take_error(&self) -> Option<String> {
self.error.lock().expect("error lock").take().map(|e| e.msg)
}
fn install(&self, vm: &mut VM) {
let sink = self.output.clone();
vm.set_output_sink(Box::new(move |s: &str| sink.write(s)));
vm.set_numeric_hook(Arc::new(numeric));
let err_cell = Arc::clone(&self.error);
let open = Arc::clone(&self.catches);
let pending = Arc::clone(&self.pending);
let current = Arc::clone(&self.current);
let interp = Arc::clone(&self.interp);
let out = self.output.clone();
vm.set_extension_handler(Box::new(move |vm: &mut VM, id: u16, arg: u8| {
if id == ext::CATCH_END {
open.lock().expect("catch lock").pop();
return;
}
if id == ext::PUTS {
let mut text = to_tcl_string(&vm.pop());
if arg == 1 {
text.push('\n');
}
out.write(&text);
vm.push(Value::Str(Arc::new(String::new())));
return;
}
if coro::is_op(id) {
let name = current.lock().expect("coroutine lock").clone();
if let Some(request) = coro::extension(vm, id, arg, name.as_deref()) {
*pending.lock().expect("request lock") = Some(request);
vm.request_halt();
}
return;
}
let outcome = match id {
ext::EVAL => eval_op(&interp, vm, arg),
ext::FFI_CALL => ffi_op(vm, arg).map_err(TclError::plain),
_ => extension(vm, id, arg).map_err(TclError::plain),
};
if let Err(e) = outcome {
*err_cell.lock().expect("error lock") = Some(e);
vm.push(Value::Undef);
vm.request_halt();
}
}));
let entered = Arc::clone(&self.catches);
vm.set_extension_wide_handler(Box::new(move |vm: &mut VM, id: u16, payload: usize| {
if id == ext_wide::DBG_LINE {
crate::dap::at_line(vm, payload);
return;
}
if id == ext_wide::CATCH {
entered.lock().expect("catch lock").push(CatchFrame {
handler: payload,
stack: vm.stack.len(),
frames: vm.frames.len(),
});
}
}));
if jit_enabled() {
vm.enable_tracing_jit();
}
}
}
fn jit_enabled() -> bool {
!matches!(
std::env::var("TCLRS_JIT").as_deref(),
Ok("off") | Ok("0") | Ok("no")
)
}
fn ffi_op(vm: &mut VM, argc: u8) -> Result<(), String> {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
let (name, args) = values.split_first().expect("the name is pushed first");
let result = crate::rust_ffi::call(&to_tcl_string(name), args)?;
vm.push(result);
Ok(())
}
fn eval_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let mut args = Vec::with_capacity(argc as usize);
for _ in 0..argc {
args.push(to_tcl_string(&vm.pop()));
}
args.reverse();
let src = if args.len() == 1 {
args.remove(0)
} else {
crate::cmd_list::concat(&args)
};
flush(&vm.chunk, interp, &vm.globals);
let result = run_source(interp, &src);
let globals = seed(&vm.chunk, interp);
vm.globals = globals;
vm.push(result?);
Ok(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Park {
Running,
AtYield,
AtYieldTo,
}
struct Context {
vm: Option<VM>,
catches: Vec<CatchFrame>,
name: Option<String>,
resumer: Option<usize>,
park: Park,
}
struct Machine {
hooks: Hooks,
chunk: Chunk,
contexts: Vec<Context>,
live: HashMap<String, usize>,
created: HashSet<String>,
globals: Vec<Value>,
current: usize,
}
impl Machine {
fn run(shared: &Shared, chunk: Chunk) -> Result<Value, TclError> {
let hooks = Hooks::new(Arc::clone(shared));
let mut main = VM::new(chunk.clone());
hooks.install(&mut main);
let globals = seed(&chunk, shared);
let mut machine = Machine {
hooks,
chunk,
contexts: vec![Context {
vm: Some(main),
catches: Vec::new(),
name: None,
resumer: None,
park: Park::Running,
}],
live: HashMap::new(),
created: HashSet::new(),
globals,
current: 0,
};
let outcome = machine.drive();
flush(&machine.chunk, shared, &machine.globals);
machine.hooks.output.flush();
match outcome? {
VMResult::Ok(v) => Ok(v),
VMResult::Halted => Ok(Value::Str(Arc::new(String::new()))),
VMResult::Error(e) => Err(TclError::plain(e)),
}
}
fn drive(&mut self) -> Result<VMResult, TclError> {
loop {
let outcome = self.run_current();
let raised = self
.hooks
.error
.lock()
.expect("error lock")
.take()
.or_else(|| match &outcome {
VMResult::Error(e) => Some(TclError::plain(e.clone())),
_ => None,
});
if let Some(e) = raised {
self.raise(e)?;
continue;
}
let request = self.hooks.pending.lock().expect("request lock").take();
if let Some(request) = request {
if let VMResult::Ok(v) = outcome {
self.vm(self.current).stack.push(v);
}
if let Err(e) = self.service(request) {
self.raise(e)?;
}
continue;
}
if self.current == 0 {
return Ok(outcome);
}
self.retire(outcome);
}
}
fn run_current(&mut self) -> VMResult {
let current = self.current;
*self.hooks.current.lock().expect("coroutine lock") = self.contexts[current].name.clone();
*self.hooks.catches.lock().expect("catch lock") =
std::mem::take(&mut self.contexts[current].catches);
let globals = std::mem::take(&mut self.globals);
let vm = self.vm(current);
vm.globals = globals;
vm.clear_halt();
let outcome = vm.run();
let globals = std::mem::take(&mut vm.globals);
self.globals = globals;
self.contexts[current].catches =
std::mem::take(&mut self.hooks.catches.lock().expect("catch lock"));
outcome
}
fn vm(&mut self, context: usize) -> &mut VM {
self.contexts[context].vm.as_mut().expect("live context")
}
fn raise(&mut self, e: TclError) -> Result<(), TclError> {
loop {
if let Some(frame) = self.contexts[self.current].catches.pop() {
let vm = self.vm(self.current);
vm.frames.truncate(frame.frames);
vm.stack.truncate(frame.stack);
vm.stack.resize(frame.stack, Value::Undef);
vm.push(Value::Str(Arc::new(e.msg)));
vm.ip = frame.handler;
return Ok(());
}
if self.current == 0 {
return Err(e);
}
match self.discard(self.current) {
Some(resumer) => self.current = resumer,
None => return Err(e),
}
}
}
fn retire(&mut self, outcome: VMResult) {
let result = match outcome {
VMResult::Ok(v) => v,
_ => Value::Str(Arc::new(String::new())),
};
let resumer = self
.discard(self.current)
.expect("a running coroutine has a resumer");
self.vm(resumer).stack.push(result);
self.current = resumer;
}
fn discard(&mut self, context: usize) -> Option<usize> {
if let Some(name) = self.contexts[context].name.take() {
self.live.remove(&name);
}
self.contexts[context].vm = None;
self.contexts[context].catches.clear();
self.contexts[context].resumer.take()
}
fn service(&mut self, request: Request) -> Result<(), TclError> {
self.service_inner(request).map_err(TclError::plain)
}
fn service_inner(&mut self, request: Request) -> Result<(), String> {
match request {
Request::Create {
name,
command,
args,
} => self.create(name, &command, args),
Request::Resume { name, args } => {
let target = self.suspended(&name)?;
let value = self.resumption(target, &name, args)?;
let resumer = self.current;
self.enter(target, value, Some(resumer));
Ok(())
}
Request::Yield(value) => {
self.in_coroutine("yield")?;
self.contexts[self.current].park = Park::AtYield;
let resumer = self.contexts[self.current]
.resumer
.take()
.expect("a running coroutine has a resumer");
self.vm(resumer).stack.push(value);
self.current = resumer;
Ok(())
}
Request::YieldTo { name, args } => {
self.in_coroutine("yieldto")?;
let name = name.strip_prefix("::").unwrap_or(&name).to_string();
if !self.created.contains(&name) {
return Err(format!(
"\"yieldto {name}\": ceding control to a command that is not a \
coroutine of this script is not supported"
));
}
let target = self.suspended(&name)?;
let value = self.resumption(target, &name, args)?;
self.contexts[self.current].park = Park::AtYieldTo;
let inherited = self.contexts[self.current].resumer.take();
self.enter(target, value, inherited);
Ok(())
}
}
}
fn create(&mut self, name: String, command: &str, args: Vec<Value>) -> Result<(), String> {
let entry = self
.chunk
.names
.iter()
.position(|n| n == command)
.and_then(|idx| self.chunk.find_sub(idx as u16))
.ok_or_else(|| format!("invalid command name \"{command}\""))?;
let mut vm = VM::new(self.chunk.clone());
self.hooks.install(&mut vm);
let base = vm.stack.len();
for a in args {
vm.stack.push(a);
}
vm.frames.push(Frame {
return_ip: self.chunk.ops.len(),
stack_base: base,
slots: Vec::new(),
});
vm.ip = entry;
if let Some(&old) = self.live.get(&name) {
self.discard(old);
}
let context = self.contexts.len();
self.contexts.push(Context {
vm: Some(vm),
catches: Vec::new(),
name: Some(name.clone()),
resumer: Some(self.current),
park: Park::Running,
});
self.created.insert(name.clone());
self.live.insert(name, context);
self.current = context;
Ok(())
}
fn enter(&mut self, target: usize, value: Value, resumer: Option<usize>) {
self.vm(target).stack.push(value);
self.contexts[target].resumer = resumer;
self.contexts[target].park = Park::Running;
self.current = target;
}
fn suspended(&self, name: &str) -> Result<usize, String> {
let Some(&context) = self.live.get(name) else {
return Err(format!("invalid command name \"{name}\""));
};
if self.contexts[context].park == Park::Running {
return Err(format!("coroutine \"{name}\" is already running"));
}
Ok(context)
}
fn resumption(&self, target: usize, name: &str, args: Vec<Value>) -> Result<Value, String> {
match self.contexts[target].park {
Park::AtYieldTo => {
let words: Vec<String> = args.iter().map(to_tcl_string).collect();
Ok(Value::Str(Arc::new(list::join(&words))))
}
_ => match <[Value; 1]>::try_from(args) {
Ok([value]) => Ok(value),
Err(rest) if rest.is_empty() => Ok(Value::Str(Arc::new(String::new()))),
Err(_) => Err(format!("wrong # args: should be \"{name} ?arg?\"")),
},
}
}
fn in_coroutine(&self, command: &str) -> Result<(), String> {
if self.contexts[self.current].name.is_none() {
return Err(format!("{command} can only be called in a coroutine"));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Num {
Int(i64),
Float(f64),
}
impl Num {
fn as_f64(self) -> f64 {
match self {
Num::Int(i) => i as f64,
Num::Float(f) => f,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NotNumeric {
Unparsable,
TooLarge,
}
fn tcl_num(v: &Value) -> Result<Num, NotNumeric> {
match v {
Value::Int(i) => Ok(Num::Int(*i)),
Value::Float(f) => Ok(Num::Float(*f)),
Value::Bool(b) => Ok(Num::Int(*b as i64)),
_ => parse_number(v.as_str_cow().trim()),
}
}
fn approx_num(v: &Value) -> Option<Num> {
if let Ok(n) = tcl_num(v) {
return Some(n);
}
let text = v.as_str_cow();
let body = text.trim();
let (sign, digits) = match body.strip_prefix('-') {
Some(rest) => (-1.0, rest),
None => (1.0, body.strip_prefix('+').unwrap_or(body)),
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
digits.parse::<f64>().ok().map(|f| Num::Float(sign * f))
}
pub(crate) fn parse_number(text: &str) -> Result<Num, NotNumeric> {
if text.is_empty() {
return Err(NotNumeric::Unparsable);
}
let (sign, body) = match text.as_bytes()[0] {
b'-' => (-1i64, &text[1..]),
b'+' => (1, &text[1..]),
_ => (1, text),
};
let radix = match body.as_bytes() {
[b'0', k, _, ..] => match k.to_ascii_lowercase() {
b'x' => Some(16),
b'o' => Some(8),
b'b' => Some(2),
b'd' => Some(10),
_ => None,
},
_ => None,
};
let cleaned;
let body = if body.contains('_') {
match without_separators(body, radix.unwrap_or(10)) {
Some(text) => {
cleaned = text;
cleaned.as_str()
}
None => return Err(NotNumeric::Unparsable),
}
} else {
body
};
if let Some(radix) = radix {
let digits = &body[2..];
return match i64::from_str_radix(digits, radix) {
Ok(v) => Ok(Num::Int(sign * v)),
Err(_) if !digits.is_empty() && digits.chars().all(|c| c.is_digit(radix)) => {
Err(NotNumeric::TooLarge)
}
Err(_) => Err(NotNumeric::Unparsable),
};
}
if let Ok(i) = body.parse::<i64>() {
return Ok(Num::Int(sign * i));
}
if !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit()) {
return Err(NotNumeric::TooLarge);
}
body.parse::<f64>()
.map(|f| Num::Float(sign as f64 * f))
.map_err(|_| NotNumeric::Unparsable)
}
fn without_separators(body: &str, radix: u32) -> Option<String> {
let bytes = body.as_bytes();
let digit = |i: usize| -> bool { bytes.get(i).is_some_and(|b| (*b as char).is_digit(radix)) };
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'_' {
i += 1;
continue;
}
let run_start = i;
while i < bytes.len() && bytes[i] == b'_' {
i += 1;
}
if run_start == 0 || !digit(run_start - 1) || !digit(i) {
return None;
}
}
Some(body.replace('_', ""))
}
pub(crate) fn tcl_bool(v: &Value) -> Result<bool, String> {
match v {
Value::Int(i) => return Ok(*i != 0),
Value::Bool(b) => return Ok(*b),
Value::Float(f) => return float_bool(*f),
_ => {}
}
let text = v.as_str_cow();
if let Some(b) = boolean_word(&text) {
return Ok(b);
}
match parse_number(text.trim()) {
Ok(Num::Int(i)) => Ok(i != 0),
Ok(Num::Float(f)) => float_bool(f),
Err(NotNumeric::TooLarge) => Ok(true),
Err(NotNumeric::Unparsable) => Err(format!(
"expected boolean value but got {}",
named(&text, 50)
)),
}
}
fn float_bool(f: f64) -> Result<bool, String> {
if f.is_nan() {
return Err("floating point value is Not a Number".to_string());
}
Ok(f != 0.0)
}
fn boolean_word(text: &str) -> Option<bool> {
if text.is_empty() || text.len() > 5 {
return None;
}
if text == "0" {
return Some(false);
}
if text == "1" {
return Some(true);
}
let lower = text.to_ascii_lowercase();
if !lower.bytes().all(|b| b"aeflnorstuy".contains(&b)) {
return None;
}
for (word, value) in [
("yes", true),
("no", false),
("true", true),
("false", false),
("on", true),
("off", false),
] {
let shortest = if word.starts_with('o') { 2 } else { 1 };
if lower.len() >= shortest && word.starts_with(&lower) {
return Some(value);
}
}
None
}
pub(crate) fn named(text: &str, limit: usize) -> String {
if list::looks_like_a_list(text) {
return "a list".to_string();
}
let mut end = text.len().min(limit);
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
format!("\"{}\"", &text[..end])
}
pub(crate) fn tcl_int(v: &Value) -> Result<i64, String> {
if let Value::Int(i) = v {
return Ok(*i);
}
let text = to_tcl_string(v);
match parse_number(text.trim()) {
Ok(Num::Int(i)) => Ok(i),
Err(NotNumeric::TooLarge) => Err(too_large()),
_ => Err(format!("expected integer but got {}", named(&text, 50))),
}
}
fn numeric(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
let cmp = matches!(
op,
NumOp::Lt | NumOp::Gt | NumOp::Le | NumOp::Ge | NumOp::Eq | NumOp::Ne
);
if cmp {
let ordering = match (approx_num(a), approx_num(b)) {
(Some(Num::Int(i)), Some(Num::Int(j))) => i.cmp(&j),
(Some(p), Some(q)) => p
.as_f64()
.partial_cmp(&q.as_f64())
.unwrap_or(std::cmp::Ordering::Greater),
_ => a.as_str_cow().cmp(&b.as_str_cow()),
};
let truth = match op {
NumOp::Lt => ordering.is_lt(),
NumOp::Gt => ordering.is_gt(),
NumOp::Le => ordering.is_le(),
NumOp::Ge => ordering.is_ge(),
NumOp::Eq => ordering.is_eq(),
_ => !ordering.is_eq(),
};
return Ok(Value::Int(truth as i64));
}
let sym = match op {
NumOp::Add => "+",
NumOp::Sub => "-",
NumOp::Mul => "*",
NumOp::Div => "/",
NumOp::Mod => "%",
NumOp::Pow => "**",
NumOp::Neg => "-",
_ => "?",
};
let x = tcl_num(a).map_err(|why| operand_error(why, a, sym))?;
let y = if matches!(op, NumOp::Neg) {
Num::Int(0)
} else {
tcl_num(b).map_err(|why| operand_error(why, b, sym))?
};
let value = match (op, x, y) {
(NumOp::Neg, Num::Int(i), _) => i.checked_neg().map(Value::Int).ok_or_else(too_large)?,
(NumOp::Neg, Num::Float(f), _) => Value::Float(-f),
(_, Num::Int(i), Num::Int(j)) => {
let folded = match op {
NumOp::Add => i.checked_add(j),
NumOp::Sub => i.checked_sub(j),
NumOp::Mul => i.checked_mul(j),
_ => return Err(format!("unsupported integer operation {sym}")),
};
Value::Int(folded.ok_or_else(too_large)?)
}
(_, p, q) => {
let (p, q) = (p.as_f64(), q.as_f64());
Value::Float(match op {
NumOp::Add => p + q,
NumOp::Sub => p - q,
NumOp::Mul => p * q,
_ => return Err(format!("unsupported operation {sym}")),
})
}
};
Ok(value)
}
fn non_numeric(v: &Value, op: &str) -> String {
format!(
"can't use non-numeric string as operand of \"{op}\": \"{}\"",
v.as_str_cow()
)
}
fn operand_error(why: NotNumeric, v: &Value, op: &str) -> String {
match why {
NotNumeric::TooLarge => too_large(),
NotNumeric::Unparsable => non_numeric(v, op),
}
}
fn too_large() -> String {
"integer value too large to represent".to_string()
}
fn extension(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::DIV | ext::MOD | ext::POW => {
let b = vm.pop();
let a = vm.pop();
let x = tcl_num(&a).map_err(|why| operand_error(why, &a, sym_of(id)))?;
let y = tcl_num(&b).map_err(|why| operand_error(why, &b, sym_of(id)))?;
vm.push(arith(id, x, y)?);
Ok(())
}
ext::BOOL => {
let v = vm.pop();
let truth = if arg == 1 {
match tcl_num(&v) {
Ok(Num::Int(i)) => i == 0,
Ok(Num::Float(f)) => !float_bool(f)?,
Err(NotNumeric::TooLarge) => return Err(too_large()),
Err(NotNumeric::Unparsable) => {
!boolean_word(&v.as_str_cow()).ok_or_else(|| non_numeric(&v, "!"))?
}
}
} else {
tcl_bool(&v)?
};
vm.push(Value::Int(truth as i64));
Ok(())
}
ext::IN | ext::NI => {
let haystack = vm.pop();
let needle = vm.pop();
let elements = crate::list::split(&haystack.as_str_cow())?;
let needle = to_tcl_string(&needle);
let found = elements.contains(&needle);
vm.push(Value::Int(i64::from(found == (id == ext::IN))));
Ok(())
}
ext::STR_CMP => {
let b = to_tcl_string(&vm.pop());
let a = to_tcl_string(&vm.pop());
let hit = match arg {
0 => a < b,
1 => a > b,
2 => a <= b,
3 => a >= b,
4 => a == b,
_ => a != b,
};
vm.push(Value::Int(hit as i64));
Ok(())
}
ext::MATCH => {
let pattern = to_tcl_string(&vm.pop());
let subject = to_tcl_string(&vm.pop());
let hit = if arg == 1 {
list::glob_match(&pattern, &subject)
} else {
subject == pattern
};
vm.push(Value::Int(hit as i64));
Ok(())
}
ext::ERROR => Err(to_tcl_string(&vm.pop())),
id if id >= ext::STRING_BASE => crate::cmd_string::extension(vm, id, arg),
id if id >= ext::ASSOC_BASE => crate::assoc::extension(vm, id, arg),
id if id >= ext::LIST_BASE => crate::cmd_list::run(vm, id, arg),
other => Err(format!("unknown extension op {other}")),
}
}
fn sym_of(id: u16) -> &'static str {
match id {
ext::DIV => "/",
ext::MOD => "%",
_ => "**",
}
}
fn arith(id: u16, x: Num, y: Num) -> Result<Value, String> {
match (id, x, y) {
(ext::DIV, Num::Int(_), Num::Int(0)) | (ext::MOD, Num::Int(_), Num::Int(0)) => {
Err("divide by zero".to_string())
}
(ext::DIV, Num::Int(i64::MIN), Num::Int(-1)) => Err(too_large()),
(ext::DIV, Num::Int(i), Num::Int(j)) => Ok(Value::Int(
i.div_euclid(j)
- i64::from(
j < 0 && i.rem_euclid(j) != 0,
),
)),
(ext::MOD, Num::Int(i), Num::Int(j)) => {
let r = i.checked_rem(j).unwrap_or(0);
Ok(Value::Int(if r != 0 && (r < 0) != (j < 0) {
r + j
} else {
r
}))
}
(ext::POW, Num::Int(i), Num::Int(j)) if j >= 0 => {
let exp = u32::try_from(j).map_err(|_| too_large())?;
i.checked_pow(exp).map(Value::Int).ok_or_else(too_large)
}
(ext::POW, Num::Int(i), Num::Int(j)) => match i {
0 => Err("exponentiation of zero by negative power".to_string()),
1 => Ok(Value::Int(1)),
-1 => Ok(Value::Int(if j % 2 == 0 { 1 } else { -1 })),
_ => Ok(Value::Int(0)),
},
(ext::DIV, p, q) => Ok(Value::Float(p.as_f64() / q.as_f64())),
(ext::MOD, _, _) => Err("can't use floating-point value as operand of \"%\"".to_string()),
(_, p, q) => {
if p.as_f64() == 0.0 && q.as_f64() < 0.0 {
return Err("exponentiation of zero by negative power".to_string());
}
Ok(Value::Float(p.as_f64().powf(q.as_f64())))
}
}
}
pub(crate) fn var_cell(vm: &mut VM, place: Place) -> Option<&mut Value> {
match place {
Place::Global(index) => {
let index = index as usize;
if index >= vm.globals.len() {
vm.globals.resize(index + 1, Value::Undef);
}
Some(&mut vm.globals[index])
}
Place::Slot(slot) => {
let frame = vm.frames.last_mut()?;
let slot = slot as usize;
if slot >= frame.slots.len() {
frame.slots.resize(slot + 1, Value::Undef);
}
Some(&mut frame.slots[slot])
}
}
}
pub(crate) fn take_var(vm: &mut VM, place: Place) -> Value {
match var_cell(vm, place) {
Some(value) => std::mem::replace(value, Value::Undef),
None => Value::Undef,
}
}
pub(crate) fn place_of(vm: &mut VM, slot_form: bool) -> Result<Place, String> {
let operand = vm.pop();
place_at(&operand, slot_form)
}
pub(crate) fn place_at(operand: &Value, slot_form: bool) -> Result<Place, String> {
match operand {
Value::Int(index) => Ok(if slot_form {
Place::Slot(*index as u16)
} else {
Place::Global(*index as u16)
}),
other => Err(format!("not a variable place: {other:?}")),
}
}
pub(crate) fn tcl_str(v: &Value) -> Cow<'_, str> {
match v {
Value::Float(f) => Cow::Owned(format_double(*f)),
Value::Bool(b) => Cow::Borrowed(if *b { "1" } else { "0" }),
other => other.as_str_cow(),
}
}
pub fn to_tcl_string(v: &Value) -> String {
tcl_str(v).into_owned()
}
pub fn format_double(f: f64) -> String {
if f.is_nan() {
return "NaN".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "Inf" } else { "-Inf" }.to_string();
}
let mag = f.abs();
if mag != 0.0 && !(1e-4..1e17).contains(&mag) {
let raw = format!("{f:e}"); let (mantissa, exponent) = raw.split_once('e').expect("exponential form");
let (sign, digits) = match exponent.strip_prefix('-') {
Some(rest) => ('-', rest),
None => ('+', exponent),
};
return format!("{mantissa}e{sign}{digits}");
}
let plain = format!("{f}");
if plain.contains(['.', 'e', 'n', 'i']) {
plain
} else {
format!("{plain}.0")
}
}