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 num_bigint::BigInt;
use num_traits::{FromPrimitive, Signed, ToPrimitive, Zero};
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,
running: Vec<Option<String>>,
}
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,
running: Vec::new(),
})),
}
}
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_sited_numeric_hook(Arc::new(|call: fusevm::NumericCall<'_>| {
if is_incr_site(call.chunk, call.ip) {
if let Some(e) = incr_operand_error(call.a, call.b) {
return Err(e);
}
}
numeric(call.op, call.a, call.b)
}));
vm.set_undef_hook(Arc::new(|read: fusevm::UndefRead<'_>| {
if tolerates_undef(read.chunk, read.ip) {
return Ok(Value::Undef);
}
match read.name {
Some(name) if !name.starts_with('\u{0}') => {
Err(format!("can't read \"{name}\": no such variable"))
}
_ => Ok(Value::Undef),
}
}));
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),
crate::cmd_info::ext::NAMES => info_names_op(&interp, vm, arg),
ext::EVAL_FRAME => eval_frame_op(&interp, vm, arg),
ext::UPLEVEL => uplevel_op(&interp, vm, arg),
ext::APPLY => apply_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);
let wide_err = Arc::clone(&self.error);
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::ERROR_AT {
let msg = to_tcl_string(&vm.pop());
*wide_err.lock().expect("error lock") = Some(TclError {
msg,
line: Some(payload),
});
vm.push(Value::Undef);
vm.request_halt();
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 info_names_op(interp: &Shared, vm: &mut VM, which: u8) -> Result<(), TclError> {
let given = matches!(vm.pop(), Value::Int(1));
let pattern = to_tcl_string(&vm.pop());
let filter = given.then_some(pattern.as_str());
let mut names: Vec<String> = match which {
0 => crate::compiler::Compiler::BUILTINS
.iter()
.map(|s| (*s).to_string())
.chain(chunk_procs(vm))
.collect(),
1 => chunk_procs(vm).collect(),
_ => {
let held: Vec<String> = interp
.lock()
.expect("interpreter lock")
.globals
.keys()
.cloned()
.collect();
held.into_iter()
.chain(vm.chunk.names.iter().enumerate().filter_map(|(i, name)| {
let set = !matches!(vm.globals.get(i), None | Some(Value::Undef));
set.then(|| name.clone())
}))
.filter(|name| !name.starts_with('\u{0}'))
.collect()
}
};
if let Some(p) = filter {
names.retain(|name| crate::list::glob_match(p, name));
}
names.sort();
names.dedup();
vm.push(Value::Str(Arc::new(crate::list::join(&names))));
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(())
}
fn eval_frame_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 declared = args.remove(0);
let src = script_of(args);
let up = levels(vm).first().copied().unwrap_or(0);
run_in_frame(interp, vm, &src, up, &declared)
}
fn uplevel_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 declared = args.remove(0);
let level = args.remove(0);
let src = script_of(args);
let ups = levels(vm);
let up = match parse_level(&level, ups.len()) {
Some(Level::Global) => {
flush(&vm.chunk, interp, &vm.globals);
let result = run_source(interp, &src);
vm.globals = seed(&vm.chunk, interp);
vm.push(result?);
return Ok(());
}
Some(Level::Up(out)) => match ups.get(out) {
Some(&up) => up,
None => return Err(TclError::plain(format!("bad level \"{level}\""))),
},
None => return Err(TclError::plain(format!("bad level \"{level}\""))),
};
run_in_frame(interp, vm, &src, up, &declared)
}
fn levels(vm: &VM) -> Vec<usize> {
let n = vm.frames.len();
(0..n)
.filter(|&up| vm.frames[n - 1 - up].entry_ip.is_some())
.collect()
}
enum Level {
Global,
Up(usize),
}
fn parse_level(word: &str, depth: usize) -> Option<Level> {
if let Some(abs) = word.strip_prefix('#') {
let abs: usize = abs.parse().ok()?;
if abs == 0 {
return Some(Level::Global);
}
return depth.checked_sub(abs).map(Level::Up);
}
let rel: usize = word.parse().ok()?;
if rel > depth {
return None;
}
if rel == depth {
Some(Level::Global)
} else {
Some(Level::Up(rel))
}
}
fn script_of(mut args: Vec<String>) -> String {
if args.len() == 1 {
args.remove(0)
} else {
crate::cmd_list::concat(&args)
}
}
fn run_in_frame(
interp: &Shared,
vm: &mut VM,
src: &str,
up: usize,
declared: &str,
) -> Result<(), TclError> {
let names: Vec<String> = vm.slot_names_at(up).to_vec();
let frame = match vm.frames.len().checked_sub(up + 1) {
Some(_) if names.is_empty() => {
flush(&vm.chunk, interp, &vm.globals);
let result = run_source(interp, src);
vm.globals = seed(&vm.chunk, interp);
vm.push(result?);
return Ok(());
}
Some(index) => index,
None => return Err(TclError::plain("bad level".to_string())),
};
let declared = crate::list::split(declared).unwrap_or_default();
flush(&vm.chunk, interp, &vm.globals);
let outer = std::mem::take(&mut interp.lock().expect("interpreter lock").globals);
let mut view: HashMap<String, Value> = HashMap::new();
for name in &declared {
if let Some(v) = outer.get(name) {
view.insert(name.clone(), v.clone());
}
}
for (slot, name) in names.iter().enumerate() {
if name.is_empty() {
continue;
}
match vm.frames[frame].slots.get(slot) {
Some(v) if *v != Value::Undef => {
view.insert(name.clone(), v.clone());
}
_ => {
view.remove(name);
}
}
}
interp.lock().expect("interpreter lock").globals = view;
let result = run_source(interp, src);
let after = std::mem::take(&mut interp.lock().expect("interpreter lock").globals);
for (slot, name) in names.iter().enumerate() {
if name.is_empty() {
continue;
}
let value = after.get(name).cloned().unwrap_or(Value::Undef);
let slots = &mut vm.frames[frame].slots;
if slot >= slots.len() {
slots.resize(slot + 1, Value::Undef);
}
slots[slot] = value;
}
let mut outer = outer;
for name in &declared {
match after.get(name) {
Some(v) => outer.insert(name.clone(), v.clone()),
None => outer.remove(name),
};
}
interp.lock().expect("interpreter lock").globals = outer;
vm.globals = seed(&vm.chunk, interp);
vm.push(result?);
Ok(())
}
fn apply_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 lambda = args.remove(0);
let parts = crate::list::split(&lambda)
.map_err(|_| TclError::plain(bad_lambda(&lambda)))?;
let (params, body) = match parts.as_slice() {
[params, body] => (params, body),
[params, body, ns] if ns == "::" || ns.is_empty() => (params, body),
[_, _, ns] => {
return Err(TclError::plain(format!(
"the namespace \"{ns}\" of a lambda is not supported yet: this frontend has only \"::\""
)))
}
_ => return Err(TclError::plain(bad_lambda(&lambda))),
};
const NAME: &str = "\u{0}apply";
let mut src = String::with_capacity(body.len() + params.len() + 32);
src.push_str("proc ");
src.push_str(NAME);
src.push(' ');
src.push_str(&crate::list::quote(params, false));
src.push(' ');
src.push_str(&crate::list::quote(body, false));
src.push('\n');
src.push_str(NAME);
for a in &args {
src.push(' ');
src.push_str(&crate::list::quote(a, false));
}
flush(&vm.chunk, interp, &vm.globals);
let result = run_source(interp, &src);
vm.globals = seed(&vm.chunk, interp);
vm.push(result.map_err(|e| TclError::plain(rename_lambda(&e.msg)))?);
Ok(())
}
fn bad_lambda(lambda: &str) -> String {
format!("can't interpret \"{lambda}\" as a lambda expression")
}
fn rename_lambda(msg: &str) -> String {
msg.replace("\u{0}apply", "apply lambdaExpr")
}
#[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;
let name = self.contexts[current].name.clone();
*self.hooks.current.lock().expect("coroutine lock") = 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);
self.hooks
.interp
.lock()
.expect("interpreter lock")
.running
.push(name);
let vm = self.vm(current);
vm.globals = globals;
vm.clear_halt();
let outcome = vm.run();
let globals = std::mem::take(&mut vm.globals);
self.hooks
.interp
.lock()
.expect("interpreter lock")
.running
.pop();
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(),
entry_ip: Some(entry),
});
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_some() {
return Ok(());
}
let nested = self
.hooks
.interp
.lock()
.expect("interpreter lock")
.running
.iter()
.any(Option::is_some);
if nested {
return Err(format!(
"{command} inside a script run by \"eval\", \"uplevel\" or \"apply\" is not \
supported: a coroutine cannot suspend across one"
));
}
Err(format!("{command} can only be called in a coroutine"))
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Num {
Int(i64),
Float(f64),
Big(BigInt),
}
impl Num {
fn as_f64(&self) -> f64 {
match self {
Num::Int(i) => *i as f64,
Num::Float(f) => *f,
Num::Big(b) => b.to_f64().unwrap_or(f64::INFINITY),
}
}
fn as_big(&self) -> Option<BigInt> {
match self {
Num::Int(i) => Some(BigInt::from(*i)),
Num::Big(b) => Some(b.clone()),
Num::Float(_) => None,
}
}
fn is_big(&self) -> bool {
matches!(self, Num::Big(_))
}
}
fn big_cmp(p: &Num, q: &Num) -> Option<std::cmp::Ordering> {
match (p, q) {
(Num::Float(f), _) | (_, Num::Float(f)) if f.is_nan() => None,
(Num::Float(f), _) if f.is_infinite() => Some(if *f < 0.0 {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}),
(_, Num::Float(f)) if f.is_infinite() => Some(if *f < 0.0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
}),
(left, Num::Float(f)) => {
let whole = BigInt::from_f64(f.trunc())?;
Some(match left.as_big()?.cmp(&whole) {
std::cmp::Ordering::Equal => 0.0.partial_cmp(&(f - f.trunc()))?,
other => other,
})
}
(Num::Float(f), right) => {
let whole = BigInt::from_f64(f.trunc())?;
Some(match whole.cmp(&right.as_big()?) {
std::cmp::Ordering::Equal => (f - f.trunc()).partial_cmp(&0.0)?,
other => other,
})
}
(left, right) => Some(left.as_big()?.cmp(&right.as_big()?)),
}
}
pub(crate) fn from_big(b: BigInt) -> Value {
match i64::try_from(&b) {
Ok(i) => Value::Int(i),
Err(_) => Value::Str(Arc::new(b.to_string())),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NotNumeric {
Unparsable,
}
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)) => {
match BigInt::parse_bytes(digits.as_bytes(), radix) {
Some(b) => Ok(Num::Big(if sign < 0 { -b } else { b })),
None => Err(NotNumeric::Unparsable),
}
}
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 match BigInt::parse_bytes(body.as_bytes(), 10) {
Some(b) => Ok(Num::Big(if sign < 0 { -b } else { b })),
None => Err(NotNumeric::Unparsable),
};
}
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),
Ok(Num::Big(_)) => 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)
}
pub(crate) 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),
Ok(Num::Big(_)) => Err(too_large()),
_ => Err(format!("expected integer but got {}", named(&text, 50))),
}
}
pub(crate) fn incr_text(current: &str, by: &str) -> Result<String, String> {
let one = incr_operand(current)?;
let other = incr_operand(by)?;
if let (Num::Int(x), Num::Int(y)) = (&one, &other) {
if let Some(sum) = x.checked_add(*y) {
return Ok(sum.to_string());
}
}
let (x, y) = (
one.as_big().expect("an integer is never a float here"),
other.as_big().expect("an integer is never a float here"),
);
Ok(to_tcl_string(&from_big(x + y)))
}
fn incr_operand(text: &str) -> Result<Num, String> {
match parse_number(text.trim()) {
Ok(n) if !matches!(n, Num::Float(_)) => Ok(n),
_ => Err(format!("expected integer but got {}", named(text, 50))),
}
}
fn incr_operand_error(a: &Value, b: &Value) -> Option<String> {
for operand in [a, b] {
if matches!(operand, Value::Undef) {
continue;
}
let integral = matches!(
parse_number(to_tcl_string(operand).trim()),
Ok(Num::Int(_)) | Ok(Num::Big(_))
);
if !integral {
return Some(format!(
"expected integer but got {}",
named(&to_tcl_string(operand), 50)
));
}
}
None
}
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)) if p.is_big() || q.is_big() => match big_cmp(&p, &q) {
Some(ordering) => ordering,
None => return Ok(Value::Int(matches!(op, NumOp::Ne) as i64)),
},
(Some(p), Some(q)) => match p.as_f64().partial_cmp(&q.as_f64()) {
Some(ordering) => ordering,
None => return Ok(Value::Int(matches!(op, NumOp::Ne) as i64)),
},
_ => 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 unary = matches!(op, NumOp::Neg);
let left = if unary { Side::Only } else { Side::Left };
let zeroed = |v: &Value| matches!(op, NumOp::Add) && *v == Value::Undef;
let x = if zeroed(a) {
Num::Int(0)
} else {
num_operand(a, left, sym)?
};
let y = if unary || zeroed(b) {
Num::Int(0)
} else {
num_operand(b, Side::Right, sym)?
};
let value = match (op, &x, &y) {
(NumOp::Neg, Num::Float(f), _) => Value::Float(-f),
(NumOp::Neg, _, _) => from_big(-x.as_big().expect("a non-float negates as an integer")),
(_, Num::Float(_), _) | (_, _, Num::Float(_)) => {
let (p, q) = (x.as_f64(), y.as_f64());
Value::Float(match op {
NumOp::Add => p + q,
NumOp::Sub => p - q,
NumOp::Mul => p * q,
_ => return Err(format!("unsupported operation {sym}")),
})
}
_ => {
let (p, q) = (
x.as_big().expect("an integer operand"),
y.as_big().expect("an integer operand"),
);
from_big(match op {
NumOp::Add => p + q,
NumOp::Sub => p - q,
NumOp::Mul => p * q,
_ => return Err(format!("unsupported integer operation {sym}")),
})
}
};
Ok(value)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Side {
Left,
Right,
Only,
}
impl Side {
fn phrase(self) -> &'static str {
match self {
Side::Left => "as left operand of",
Side::Right => "as right operand of",
Side::Only => "as operand of",
}
}
}
fn operand(v: &Value, kind: &str, side: Side, op: &str) -> String {
let text = to_tcl_string(v);
if list::looks_like_a_list(&text) {
return format!("cannot use a list {} \"{op}\"", side.phrase());
}
format!("cannot use {kind} \"{text}\" {} \"{op}\"", side.phrase())
}
fn non_numeric(v: &Value, side: Side, op: &str) -> String {
operand(v, "non-numeric string", side, op)
}
fn non_integer(v: &Value, side: Side, op: &str) -> String {
operand(v, "floating-point value", side, op)
}
fn operand_error(why: NotNumeric, v: &Value, side: Side, op: &str) -> String {
match why {
NotNumeric::Unparsable => non_numeric(v, side, op),
}
}
fn num_operand(v: &Value, side: Side, op: &str) -> Result<Num, String> {
match tcl_num(v) {
Ok(Num::Float(f)) if f.is_nan() => {
Err(operand(v, "non-numeric floating-point value", side, op))
}
Ok(n) => Ok(n),
Err(why) => Err(operand_error(why, v, side, 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::POW => {
let b = vm.pop();
let a = vm.pop();
let x = num_operand(&a, Side::Left, sym_of(id))?;
let y = num_operand(&b, Side::Right, sym_of(id))?;
vm.push(arith(id, x, y)?);
Ok(())
}
ext::MOD => {
let b = vm.pop();
let a = vm.pop();
let x = match big_operand(&a, Side::Left, "%")? {
BigOperand::Int(i) => Num::Int(i),
BigOperand::Big(b) => Num::Big(b),
};
let y = match big_operand(&b, Side::Right, "%")? {
BigOperand::Int(i) => Num::Int(i),
BigOperand::Big(b) => Num::Big(b),
};
vm.push(arith(id, x, y)?);
Ok(())
}
ext::BIT_AND | ext::BIT_OR | ext::BIT_XOR => {
let b = vm.pop();
let a = vm.pop();
let sym = sym_of(id);
let x = big_operand(&a, Side::Left, sym)?;
let y = big_operand(&b, Side::Right, sym)?;
let value = match (x, y) {
(BigOperand::Int(x), BigOperand::Int(y)) => Value::Int(match id {
ext::BIT_AND => x & y,
ext::BIT_OR => x | y,
_ => x ^ y,
}),
(x, y) => {
let (x, y) = (x.into_big(), y.into_big());
from_big(match id {
ext::BIT_AND => x & y,
ext::BIT_OR => x | y,
_ => x ^ y,
})
}
};
vm.push(value);
Ok(())
}
ext::SHL | ext::SHR => {
let b = vm.pop();
let a = vm.pop();
let sym = sym_of(id);
let x = big_operand(&a, Side::Left, sym)?;
let by = int_operand(&b, Side::Right, sym)?;
vm.push(shift(id, x, by)?);
Ok(())
}
ext::BIT_NOT => {
let a = vm.pop();
let value = match big_operand(&a, Side::Only, "~")? {
BigOperand::Int(i) => Value::Int(!i),
BigOperand::Big(b) => from_big(!b),
};
vm.push(value);
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)) if f.is_nan() => {
return Err(operand(
&v,
"non-numeric floating-point value",
Side::Only,
"!",
))
}
Ok(Num::Float(f)) => !float_bool(f)?,
Ok(Num::Big(_)) => false,
Err(NotNumeric::Unparsable) => !boolean_word(&v.as_str_cow())
.ok_or_else(|| non_numeric(&v, Side::Only, "!"))?,
}
} 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(&to_tcl_string(&haystack))?;
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::CANON => {
let v = vm.pop();
let canonical = match v {
Value::Float(f) => Value::Str(Arc::new(nan_checked(f)?)),
other => canonical_number(other)?,
};
vm.push(canonical);
Ok(())
}
ext::UPLUS => {
let v = vm.pop();
num_operand(&v, Side::Only, "+")?;
vm.push(canonical_number(v)?);
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::INFO_BASE => crate::cmd_info::extension(vm, id, arg),
id if id >= ext::REGEXP_BASE => crate::regexp::extension(vm, id, arg),
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 => "%",
ext::BIT_AND => "&",
ext::BIT_OR => "|",
ext::BIT_XOR => "^",
ext::SHL => "<<",
ext::SHR => ">>",
ext::BIT_NOT => "~",
_ => "**",
}
}
fn int_operand(v: &Value, side: Side, op: &str) -> Result<i64, String> {
match big_operand(v, side, op)? {
BigOperand::Int(i) => Ok(i),
BigOperand::Big(b) => Err(format!(
"integer value too large to represent: {b}"
)),
}
}
enum BigOperand {
Int(i64),
Big(BigInt),
}
impl BigOperand {
fn into_big(self) -> BigInt {
match self {
BigOperand::Int(i) => BigInt::from(i),
BigOperand::Big(b) => b,
}
}
}
fn big_operand(v: &Value, side: Side, op: &str) -> Result<BigOperand, String> {
match num_operand(v, side, op)? {
Num::Int(i) => Ok(BigOperand::Int(i)),
Num::Big(b) => Ok(BigOperand::Big(b)),
Num::Float(_) => Err(non_integer(v, side, op)),
}
}
fn shift(id: u16, value: BigOperand, by: i64) -> Result<Value, String> {
if by < 0 {
return Err("negative shift argument".to_string());
}
if id == ext::SHR {
return Ok(match value {
BigOperand::Int(v) if by >= 63 => Value::Int(if v < 0 { -1 } else { 0 }),
BigOperand::Int(v) => Value::Int(v >> by),
BigOperand::Big(b) => from_big(b >> shift_distance(by)?),
});
}
Ok(match value {
BigOperand::Int(0) => Value::Int(0),
BigOperand::Int(v) if by < 64 => match v
.checked_shl(by as u32)
.filter(|shifted| shifted >> by == v)
{
Some(shifted) => Value::Int(shifted),
None => from_big(BigInt::from(v) << shift_distance(by)?),
},
BigOperand::Int(v) => from_big(BigInt::from(v) << shift_distance(by)?),
BigOperand::Big(b) => from_big(b << shift_distance(by)?),
})
}
const MAX_INT_BITS: u64 = 1 << 20;
fn shift_distance(by: i64) -> Result<usize, String> {
if by as u64 > MAX_INT_BITS {
return Err(int_too_wide());
}
Ok(by as usize)
}
fn int_too_wide() -> String {
"integer value too large to represent".to_string()
}
fn big_arith(id: u16, p: BigInt, q: BigInt) -> Result<Value, String> {
if matches!(id, ext::DIV | ext::MOD) && q.is_zero() {
return Err("divide by zero".to_string());
}
match id {
ext::DIV | ext::MOD => {
let (quotient, remainder) = (&p / &q, &p % &q);
let stepped = !remainder.is_zero() && (remainder.is_negative() != q.is_negative());
Ok(if id == ext::DIV {
from_big(if stepped { quotient - 1 } else { quotient })
} else {
from_big(if stepped { remainder + &q } else { remainder })
})
}
_ => {
if q.is_negative() {
return match () {
_ if p.is_zero() => {
Err("exponentiation of zero by negative power".to_string())
}
_ => Ok(Value::Int(0)),
};
}
let exp = u32::try_from(&q).map_err(|_| "exponent too large".to_string())?;
if p.bits() * u64::from(exp) > MAX_INT_BITS {
return Err(int_too_wide());
}
Ok(from_big(p.pow(exp)))
}
}
}
fn arith(id: u16, x: Num, y: Num) -> Result<Value, String> {
if matches!(id, ext::DIV | ext::MOD | ext::POW) && (x.is_big() || y.is_big()) {
if let (Some(p), Some(q)) = (x.as_big(), y.as_big()) {
return big_arith(id, p, q);
}
}
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)) => {
Ok(from_big(-BigInt::from(i64::MIN)))
}
(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(|_| "exponent too large".to_string())?;
match i.checked_pow(exp) {
Some(v) => Ok(Value::Int(v)),
None => big_arith(id, BigInt::from(i), BigInt::from(j)),
}
}
(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) => float_result(p.as_f64() / q.as_f64()),
(ext::MOD, _, _) => unreachable!("`%` operands are integers by now"),
(_, p, q) => {
if p.as_f64() == 0.0 && q.as_f64() < 0.0 {
return Err("exponentiation of zero by negative power".to_string());
}
float_result(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])
}
}
}
fn float_result(f: f64) -> Result<Value, String> {
if f.is_nan() {
return Err("domain error: argument not in valid range".to_string());
}
Ok(Value::Float(f))
}
fn nan_checked(f: f64) -> Result<String, String> {
if f.is_nan() {
return Err("domain error: argument not in valid range".to_string());
}
Ok(format_double(f))
}
fn canonical_number(v: Value) -> Result<Value, String> {
if matches!(v, Value::Int(_)) {
return Ok(v);
}
let text = v.as_str_cow();
match parse_number(text.trim()) {
Ok(Num::Int(i)) => Ok(Value::Int(i)),
Ok(Num::Float(f)) => Ok(Value::Str(Arc::new(nan_checked(f)?))),
Ok(Num::Big(b)) => Ok(from_big(b)),
Err(_) => {
drop(text);
Ok(v)
}
}
}
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:?}")),
}
}
static TOLERANT_READS: Mutex<Option<HashSet<(u64, usize)>>> = Mutex::new(None);
fn chunk_identity(chunk: &fusevm::Chunk) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
chunk.op_hash.hash(&mut h);
chunk.names.hash(&mut h);
h.finish() | 1
}
static INCR_SITES: Mutex<Option<HashSet<(u64, usize)>>> = Mutex::new(None);
pub(crate) type ProcParams = Vec<(String, Option<String>)>;
static PROC_TABLE: Mutex<Option<HashMap<(u64, String), ProcParams>>> = Mutex::new(None);
pub(crate) fn note_procs(chunk: &fusevm::Chunk, procs: &[(String, ProcParams)]) {
if procs.is_empty() {
return;
}
let id = chunk_identity(chunk);
let mut guard = PROC_TABLE.lock().expect("proc table lock");
let table = guard.get_or_insert_with(HashMap::new);
for (name, params) in procs {
table.insert((id, name.clone()), params.clone());
}
}
pub(crate) fn proc_params(vm: &VM, name: &str) -> Option<ProcParams> {
let id = chunk_identity(&vm.chunk);
PROC_TABLE
.lock()
.expect("proc table lock")
.as_ref()
.and_then(|t| t.get(&(id, name.to_string())).cloned())
}
fn chunk_procs(vm: &VM) -> impl Iterator<Item = String> + '_ {
let id = chunk_identity(&vm.chunk);
let names: Vec<String> = PROC_TABLE
.lock()
.expect("proc table lock")
.as_ref()
.map(|t| {
t.keys()
.filter(|(chunk, _)| *chunk == id)
.map(|(_, name)| name.clone())
.collect()
})
.unwrap_or_default();
names.into_iter()
}
pub(crate) fn current_script() -> String {
CURRENT_SCRIPT
.lock()
.expect("script lock")
.clone()
.unwrap_or_default()
}
pub fn note_script(path: &str) {
*CURRENT_SCRIPT.lock().expect("script lock") = Some(path.to_string());
}
static CURRENT_SCRIPT: Mutex<Option<String>> = Mutex::new(None);
pub(crate) fn note_incr_sites(chunk: &fusevm::Chunk, ips: &[usize]) {
if ips.is_empty() {
return;
}
let id = chunk_identity(chunk);
let mut guard = INCR_SITES.lock().expect("incr sites lock");
let set = guard.get_or_insert_with(HashSet::new);
for &ip in ips {
set.insert((id, ip));
}
}
fn is_incr_site(id: u64, ip: usize) -> bool {
INCR_SITES
.lock()
.expect("incr sites lock")
.as_ref()
.is_some_and(|set| set.contains(&(id, ip)))
}
pub(crate) fn note_tolerant_reads(chunk: &fusevm::Chunk, ips: &[usize]) {
if ips.is_empty() {
return;
}
let id = chunk_identity(chunk);
let mut guard = TOLERANT_READS.lock().expect("tolerant reads lock");
let set = guard.get_or_insert_with(HashSet::new);
for &ip in ips {
set.insert((id, ip));
}
}
fn tolerates_undef(id: u64, ip: usize) -> bool {
TOLERANT_READS
.lock()
.expect("tolerant reads lock")
.as_ref()
.is_some_and(|set| set.contains(&(id, ip)))
}
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")
}
}