use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
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,
}
pub(crate) const TCL_OK: i32 = 0;
pub(crate) const TCL_ERROR: i32 = 1;
pub(crate) const TCL_RETURN: i32 = 2;
pub(crate) const TCL_BREAK: i32 = 3;
pub(crate) const TCL_CONTINUE: i32 = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TclError {
pub msg: String,
pub line: Option<usize>,
pub code: i32,
pub level: i32,
}
impl TclError {
pub(crate) fn plain(msg: impl Into<String>) -> Self {
TclError {
msg: msg.into(),
line: None,
code: TCL_ERROR,
level: 0,
}
}
pub(crate) fn coded(code: i32, level: i32, msg: impl Into<String>) -> Self {
TclError {
msg: msg.into(),
line: None,
code,
level,
}
}
pub(crate) fn visible_code(&self) -> i32 {
if self.level > 0 {
TCL_RETURN
} else {
self.code
}
}
pub(crate) fn descend(mut self) -> Self {
if self.level > 0 {
self.level -= 1;
}
self
}
pub(crate) fn options(&self) -> String {
format!("-code {} -level {}", self.code, self.level)
}
pub(crate) fn from_options(options: &str, msg: String) -> Self {
let mut error = TclError {
msg,
line: None,
code: TCL_ERROR,
level: 0,
};
let mut words = options.split_whitespace();
while let (Some(key), Some(value)) = (words.next(), words.next()) {
match key {
"-code" => error.code = value.parse().unwrap_or(TCL_ERROR),
"-level" => error.level = value.parse().unwrap_or(0),
_ => {}
}
}
error
}
pub(crate) fn escaped(self) -> Self {
let word = match self.visible_code() {
TCL_BREAK => "break",
TCL_CONTINUE => "continue",
_ => return self,
};
TclError {
msg: format!("invoked \"{word}\" outside of a loop"),
..self
}
}
}
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)]
pub(crate) 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(),
))))
}
pub(crate) 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());
}
}
}
pub(crate) fn flush(&self) {
if let Output::Stdout(out) = self {
let _ = out.lock().expect("output lock").flush();
}
}
}
pub(crate) struct State {
pub(crate) globals: HashMap<String, Value>,
pub(crate) commands: HashMap<String, crate::procs::RuntimeProc>,
pub(crate) ns: crate::cmd_namespace::Registry,
pub(crate) running: Vec<Arc<Chunk>>,
aliases: HashMap<String, String>,
projections: Vec<Projection>,
cache: ChunkCache,
output: Output,
depth: usize,
limit: usize,
contexts: Vec<Option<String>>,
pub(crate) afters: crate::cmd_after::Afters,
}
struct Projection {
outer: HashMap<String, Value>,
declared: Vec<String>,
}
pub(crate) 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(),
commands: HashMap::new(),
ns: crate::cmd_namespace::Registry::default(),
running: Vec::new(),
aliases: HashMap::new(),
projections: Vec::new(),
cache: ChunkCache::new(),
output,
depth: 0,
limit: DEFAULT_RECURSION_LIMIT,
contexts: Vec::new(),
afters: crate::cmd_after::Afters::default(),
})),
}
}
pub fn set_recursion_limit(&mut self, limit: usize) {
self.lock().limit = limit.max(1);
}
fn outermost(outcome: Result<Value, TclError>) -> Result<String, TclError> {
match outcome {
Ok(v) => Ok(to_tcl_string(&v)),
Err(e) => {
let e = e.descend();
if e.visible_code() == TCL_OK {
Ok(e.msg)
} else {
Err(e.escaped())
}
}
}
}
pub fn eval(&mut self, src: &str) -> Result<String, TclError> {
Self::outermost(run_source(&self.shared, src))
}
pub fn run_chunk(&mut self, chunk: fusevm::Chunk) -> Result<String, TclError> {
Self::outermost(Machine::run(&self.shared, Arc::new(chunk)))
}
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
}
#[cfg(feature = "tk")]
pub(crate) fn into_shared(self) -> Shared {
self.shared
}
#[cfg(feature = "tk")]
pub(crate) fn shared_handle(&self) -> Shared {
Arc::clone(&self.shared)
}
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()
}
}
pub(crate) 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;
let projected = state.projected();
state.cache.compile_in(src, projected)
};
let result = match compiled {
Ok(chunk) => Machine::run(shared, chunk),
Err(e) => run_prefix(shared, src, e),
};
shared.lock().expect("interpreter lock").depth -= 1;
result
}
fn run_prefix(shared: &Shared, src: &str, err: TclError) -> Result<Value, TclError> {
let Some((end, line, _)) = crate::parser::valid_prefix(src) else {
return Err(err);
};
let err = TclError {
line: Some(line),
..err
};
if end == 0 {
return Err(err);
}
let compiled = {
let mut state = shared.lock().expect("interpreter lock");
let projected = state.projected();
state.cache.compile_in(&src[..end], projected)
};
match compiled.and_then(|chunk| Machine::run(shared, chunk)) {
Ok(_) => Err(err),
Err(e) => Err(e),
}
}
pub(crate) fn call_in_chunk(
shared: &Shared,
chunk: &Arc<Chunk>,
entry: usize,
actuals: Vec<Value>,
) -> Result<Value, TclError> {
{
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;
}
let result = Machine::start(shared, Arc::clone(chunk), Some((entry, actuals)));
shared.lock().expect("interpreter lock").depth -= 1;
match result {
Err(e) if e.level > 0 => {
let e = e.descend();
if e.visible_code() == TCL_OK {
Ok(Value::Str(Arc::new(e.msg)))
} else {
Err(e)
}
}
other => other,
}
}
thread_local! {
static VMS: std::cell::RefCell<Vec<(Arc<Chunk>, VM)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
const POOLED_VMS: usize = 8;
fn acquire_vm(chunk: &Arc<Chunk>) -> VM {
let pooled = VMS.with(|pool| {
let mut pool = pool.borrow_mut();
let at = pool.iter().position(|(key, _)| Arc::ptr_eq(key, chunk))?;
Some(pool.remove(at).1)
});
match pooled {
Some(mut vm) => {
let program = std::mem::take(&mut vm.chunk);
vm.reset(program);
vm
}
None => VM::new((**chunk).clone()),
}
}
fn release_vm(chunk: &Arc<Chunk>, vm: VM) {
VMS.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() >= POOLED_VMS {
pool.remove(0);
}
pool.push((Arc::clone(chunk), vm));
});
}
fn seed(chunk: &Chunk, shared: &Shared) -> Vec<Value> {
let traced = TracedIn::of(chunk);
let mut values: Vec<Value> = {
let state = shared.lock().expect("interpreter lock");
chunk
.names
.iter()
.map(|name| {
let key = state.alias_of(name);
let value = if crate::cmd_namespace::is_namespaced(name) {
state.root_global(key)
} else {
state.globals.get(key)
};
value.cloned().unwrap_or(Value::Undef)
})
.collect()
};
traced.blank_reads(&mut values);
values
}
fn flush(chunk: &Chunk, shared: &Shared, globals: &[Value]) {
let traced = TracedIn::of(chunk);
let fired = write_back(chunk, shared, globals, &traced, Boundary::End);
for (name, op) in fired {
let _ = TracedIn::fire_one(&name, op);
}
}
fn write_back(
chunk: &Chunk,
shared: &Shared,
globals: &[Value],
traced: &TracedIn,
boundary: Boundary,
) -> Vec<(String, TraceOp)> {
let mut fired = Vec::new();
let mut state = shared.lock().expect("interpreter lock");
for (slot, name) in chunk.names.iter().enumerate() {
if name.starts_with('\u{0}') {
continue;
}
let past = crate::cmd_namespace::is_namespaced(name);
let aliased = (!state.aliases.is_empty() || name.starts_with("::"))
.then(|| state.alias_of(name).to_string());
let name: &str = aliased.as_deref().unwrap_or(name);
let value = globals.get(slot).unwrap_or(&Value::Undef);
let watched = traced.at(slot);
if past && state.projected() {
match value {
Value::Undef if watched.reads || boundary == Boundary::Sync => {}
Value::Undef => {
if state.root_global(name).is_some() {
state.set_root_global(name, None);
if watched.unsets {
fired.push((name.to_string(), TraceOp::Unset));
}
}
}
value => {
let changed = state.root_global(name) != Some(value);
state.set_root_global(name, Some(value.clone()));
if changed && watched.writes {
fired.push((name.to_string(), TraceOp::Write));
}
}
}
continue;
}
match value {
Value::Undef if watched.reads || boundary == Boundary::Sync => {}
Value::Undef => {
if state.globals.remove(name).is_some() && watched.unsets {
fired.push((name.to_string(), TraceOp::Unset));
}
}
value => {
if state.globals.get(name) != Some(value) {
state.globals.insert(name.to_string(), value.clone());
if watched.writes {
fired.push((name.to_string(), TraceOp::Write));
}
}
}
}
}
for (offset, name) in overflow_names(chunk, globals).iter().enumerate() {
let value = globals.get(overflow_value_index(chunk, offset));
match value {
Some(Value::Undef) | None if boundary == Boundary::End => {
state.globals.remove(name);
}
Some(Value::Undef) | None => {}
Some(value) => {
state.globals.insert(name.clone(), value.clone());
}
}
}
fired
}
fn overflow_names(chunk: &Chunk, globals: &[Value]) -> Vec<String> {
match globals.get(chunk.names.len()) {
Some(Value::Array(names)) => names.iter().map(to_tcl_string).collect(),
_ => Vec::new(),
}
}
fn overflow_value_index(chunk: &Chunk, offset: usize) -> usize {
chunk.names.len() + 1 + offset
}
pub(crate) fn read_global(interp: &Shared, name: &str) -> Option<Value> {
let state = interp.lock().expect("interpreter lock");
let key = state.alias_of(name);
if crate::cmd_namespace::is_namespaced(name) {
return state.root_global(key).cloned();
}
state.globals.get(key).cloned()
}
impl State {
fn alias_of<'a>(&'a self, name: &'a str) -> &'a str {
let mut at = crate::cmd_namespace::store_key(name);
if self.aliases.is_empty() {
return at;
}
for _ in 0..8 {
match self.aliases.get(at) {
Some(target) if target != at => at = target,
_ => break,
}
}
at
}
}
impl State {
pub(crate) fn projected(&self) -> bool {
!self.projections.is_empty()
}
fn root_global(&self, key: &str) -> Option<&Value> {
match self.projections.first() {
Some(p) => p.outer.get(key),
None => self.globals.get(key),
}
}
fn set_root_global(&mut self, key: &str, value: Option<Value>) {
let table = match self.projections.first_mut() {
Some(p) => &mut p.outer,
None => &mut self.globals,
};
match value {
Some(v) => table.insert(key.to_string(), v),
None => table.remove(key),
};
}
pub(crate) fn frame_declared(&self) -> Option<Vec<String>> {
self.projections.last().map(|p| p.declared.clone())
}
}
pub(crate) fn alias_global(interp: &Shared, alias: &str, target: &str) -> Result<(), String> {
let alias = crate::cmd_namespace::store_key(alias).to_string();
let target = crate::cmd_namespace::store_key(target).to_string();
if alias == target {
return Err("can't upvar from variable to itself".to_string());
}
let mut state = interp.lock().expect("interpreter lock");
if let Some(value) = state.globals.get(&target).cloned() {
state.globals.insert(alias.clone(), value);
} else {
state.globals.remove(&alias);
}
state.aliases.insert(alias, target);
Ok(())
}
pub(crate) fn intern_overflow(interp: &Shared, vm: &mut VM, key: &str) -> Result<u16, String> {
let base = vm.chunk.names.len();
let mut names = overflow_names(&vm.chunk, &vm.globals);
if let Some(offset) = names.iter().position(|n| n == key) {
return index_of(base + 1 + offset);
}
names.push(key.to_string());
let offset = names.len() - 1;
let value_at = base + 1 + offset;
if vm.globals.len() <= value_at {
vm.globals.resize(value_at + 1, Value::Undef);
}
vm.globals[base] = Value::array(
names
.iter()
.map(|n| Value::Str(Arc::new(n.clone())))
.collect(),
);
vm.globals[value_at] = interp
.lock()
.expect("interpreter lock")
.globals
.get(key)
.cloned()
.unwrap_or(Value::Undef);
index_of(value_at)
}
fn index_of(index: usize) -> Result<u16, String> {
u16::try_from(index).map_err(|_| "too many variables in one chunk".to_string())
}
fn reproject(chunk: &Chunk, shared: &Shared, old: &[Value]) -> Vec<Value> {
let mut values = seed(chunk, shared);
let names = overflow_names(chunk, old);
if names.is_empty() {
return values;
}
let base = chunk.names.len();
values.resize(base + 1 + names.len(), Value::Undef);
values[base] = Value::array(
names
.iter()
.map(|n| Value::Str(Arc::new(n.clone())))
.collect(),
);
let state = shared.lock().expect("interpreter lock");
for (offset, name) in names.iter().enumerate() {
values[base + 1 + offset] = state.globals.get(name).cloned().unwrap_or(Value::Undef);
}
values
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Boundary {
Sync,
End,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Traced {
pub reads: bool,
pub writes: bool,
pub unsets: bool,
}
impl Traced {
pub fn any(self) -> bool {
self.reads || self.writes || self.unsets
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TraceOp {
Read,
Write,
Unset,
}
pub trait VarTraceSink: Send + Sync {
fn traced(&self, name: &str) -> Traced;
fn fire(&self, name: &str, op: TraceOp) -> Result<(), String>;
}
static TRACES_ARMED: AtomicBool = AtomicBool::new(false);
static TRACE_SINK: Mutex<Option<Arc<dyn VarTraceSink>>> = Mutex::new(None);
pub fn set_var_trace_sink(sink: Arc<dyn VarTraceSink>) {
*TRACE_SINK.lock().expect("trace sink lock") = Some(sink);
}
pub fn arm_var_traces(armed: bool) {
TRACES_ARMED.store(armed, Ordering::Relaxed);
}
pub fn traces_armed() -> bool {
TRACES_ARMED.load(Ordering::Relaxed)
}
fn trace_sink() -> Option<Arc<dyn VarTraceSink>> {
if !traces_armed() {
return None;
}
TRACE_SINK.lock().expect("trace sink lock").clone()
}
struct TracedIn {
entries: Vec<(usize, String, Traced)>,
}
impl TracedIn {
fn of(chunk: &Chunk) -> TracedIn {
let Some(sink) = trace_sink() else {
return TracedIn {
entries: Vec::new(),
};
};
let entries = chunk
.names
.iter()
.enumerate()
.filter(|(_, name)| !name.starts_with('\u{0}'))
.filter_map(|(slot, name)| {
let name = crate::cmd_namespace::store_key(name);
let watched = sink.traced(name);
watched.any().then(|| (slot, name.to_string(), watched))
})
.collect();
TracedIn { entries }
}
#[cfg(feature = "tk")]
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn at(&self, slot: usize) -> Traced {
self.entries
.iter()
.find(|(i, _, _)| *i == slot)
.map_or(Traced::default(), |(_, _, w)| *w)
}
fn fire_one(name: &str, op: TraceOp) -> Result<(), String> {
match trace_sink() {
Some(sink) => sink.fire(name, op),
None => Ok(()),
}
}
fn blank_reads(&self, values: &mut [Value]) {
for (slot, _, watched) in &self.entries {
if watched.reads {
if let Some(cell) = values.get_mut(*slot) {
*cell = Value::Undef;
}
}
}
}
}
#[cfg(feature = "tk")]
pub(crate) fn sync_out(shared: &Shared, vm: &mut VM) -> Result<(), String> {
if !traces_armed() {
return Ok(());
}
let traced = TracedIn::of(&vm.chunk);
if traced.is_empty() {
return Ok(());
}
let fired = write_back(&vm.chunk, shared, &vm.globals, &traced, Boundary::Sync);
for (name, op) in fired {
TracedIn::fire_one(&name, op)?;
}
project(shared, vm, &traced);
Ok(())
}
#[cfg(feature = "tk")]
pub(crate) fn sync_in(shared: &Shared, vm: &mut VM) {
if !traces_armed() {
return;
}
let traced = TracedIn::of(&vm.chunk);
if traced.is_empty() {
return;
}
project(shared, vm, &traced);
}
#[cfg(feature = "tk")]
fn project(shared: &Shared, vm: &mut VM, traced: &TracedIn) {
let state = shared.lock().expect("interpreter lock");
for (slot, name, _) in &traced.entries {
if let Some(cell) = vm.globals.get_mut(*slot) {
*cell = state.globals.get(name).cloned().unwrap_or(Value::Undef);
}
}
drop(state);
traced.blank_reads(&mut vm.globals);
}
#[cfg(feature = "tk")]
pub(crate) fn global_of(shared: &Shared, name: &str) -> Option<Value> {
shared
.lock()
.expect("interpreter lock")
.globals
.get(name)
.cloned()
}
#[cfg(feature = "tk")]
pub(crate) fn set_global_of(shared: &Shared, name: &str, value: Value) -> Result<(), String> {
let changed = {
let mut state = shared.lock().expect("interpreter lock");
let changed = state.globals.get(name) != Some(&value);
state.globals.insert(name.to_string(), value);
changed
};
if changed && traces_armed() {
TracedIn::fire_one(name, TraceOp::Write)?;
}
Ok(())
}
#[cfg(feature = "tk")]
pub(crate) fn unset_global_of(shared: &Shared, name: &str) -> bool {
let had = shared
.lock()
.expect("interpreter lock")
.globals
.remove(name)
.is_some();
if had && traces_armed() {
let _ = TracedIn::fire_one(name, TraceOp::Unset);
}
had
}
fn traced_read(shared: &Shared, name: &str) -> Option<Value> {
let sink = trace_sink()?;
if !sink.traced(name).reads {
return None;
}
sink.fire(name, TraceOp::Read).ok()?;
let state = shared.lock().expect("interpreter lock");
state.globals.get(name).cloned()
}
#[derive(Clone, Copy)]
struct CatchFrame {
kind: FrameKind,
stack: usize,
frames: usize,
}
#[derive(Clone, Copy)]
enum FrameKind {
Catch(usize),
Loop {
brk: usize,
cont: usize,
step_start: usize,
step_end: 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)
}));
let undef_interp = Arc::clone(&self.interp);
vm.set_undef_hook(Arc::new(move |read: fusevm::UndefRead<'_>| {
if traces_armed() {
if let Some(name) = read.name {
if let Some(value) = traced_read(&undef_interp, name) {
return Ok(value);
}
}
}
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 || id == ext::LOOP_LEAVE {
open.lock().expect("catch lock").pop();
return;
}
if id == ext::LOOP_ENTER {
let index = |v: Value| match v {
Value::Int(i) => i as usize,
other => to_tcl_string(&other).parse().unwrap_or(0),
};
let mark = index(vm.pop());
let cont = index(vm.pop());
let brk = index(vm.pop());
let target = |at: usize| match vm.chunk.ops.get(at) {
Some(fusevm::Op::Jump(to)) => *to,
_ => 0,
};
let (step_start, step_end) = (target(cont), target(mark));
open.lock().expect("catch lock").push(CatchFrame {
kind: FrameKind::Loop {
brk,
cont,
step_start,
step_end,
},
stack: vm.stack.len(),
frames: vm.frames.len(),
});
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 | ext::FFI_CALL | ext::PROC_DEFINE | ext::DYN_CALL | ext::EXPAND_CALL => {
interpreter_op(&interp, vm, id, arg)
}
id if crate::cmd_namespace::is_op(id) => {
crate::cmd_namespace::extension(&interp, vm, id, arg)
}
id if crate::cmd_source::is_op(id) => {
crate::cmd_source::extension(&interp, vm, id, arg)
}
ext::AFTER => crate::cmd_after::after_op(&interp, vm, arg),
ext::UPDATE => crate::cmd_after::update_op(&interp, vm, arg),
ext::VWAIT => crate::cmd_after::vwait_op(&interp, vm, arg),
ext::UPVAR => crate::cmd_scope::upvar_op(&interp, vm, arg),
ext::DICT_WITH_BIND => crate::assoc::dict_with_bind(&interp, vm),
ext::DICT_WITH_END => crate::assoc::dict_with_end(vm, arg),
ext::LINK_GET => crate::cmd_scope::link_get(vm, arg == 1),
ext::LINK_SET => crate::cmd_scope::link_set(vm),
ext::DYN_GET => crate::cmd_scope::dyn_get_op(&interp, vm, arg),
ext::DYN_SET => crate::cmd_scope::dyn_set_op(&interp, vm),
ext::DYN_UNSET => crate::cmd_scope::dyn_unset_op(&interp, vm, arg == 1),
ext::DYN_EXISTS => crate::cmd_scope::dyn_exists_op(vm, &interp),
ext::EVAL_FRAME => eval_frame_op(&interp, vm, arg),
ext::UPLEVEL => uplevel_op(&interp, vm, arg),
ext::APPLY => apply_op(&interp, vm, arg),
ext::SUBST => crate::cmd_subst::subst_op(&interp, vm, arg),
ext::LSORT => crate::cmd_list::lsort_op(&interp, vm, arg).map_err(TclError::plain),
crate::regexp::ext::REGSUB => {
crate::regexp::regsub_op(&interp, vm, arg).map_err(TclError::plain)
}
crate::cmd_info::ext::NAMES => info_names_op(&interp, vm, arg),
id if (ext::CHANNEL_BASE..ext::CHANNEL_END).contains(&id) => {
crate::cmd_channel::run(vm, id, arg, &out).map_err(TclError::plain)
}
ext::PACKAGE => package_op(&interp, vm, arg),
ext::RAISE => {
let level = to_tcl_string(&vm.pop()).parse().unwrap_or(0);
let code = to_tcl_string(&vm.pop()).parse().unwrap_or(TCL_ERROR);
Err(TclError::coded(code, level, to_tcl_string(&vm.pop())))
}
ext::RERAISE => {
let msg = to_tcl_string(&vm.pop());
let options = to_tcl_string(&vm.pop());
vm.pop(); Err(TclError::from_options(&options, msg))
}
_ => 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: (payload > 0).then_some(payload),
code: TCL_ERROR,
level: 0,
});
vm.push(Value::Undef);
vm.request_halt();
return;
}
if id == ext_wide::CATCH {
entered.lock().expect("catch lock").push(CatchFrame {
kind: FrameKind::Catch(payload),
stack: vm.stack.len(),
frames: vm.frames.len(),
});
}
}));
if jit_enabled() {
vm.enable_tracing_jit();
}
}
}
fn jit_enabled() -> bool {
static ARMED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ARMED.get_or_init(|| {
!matches!(
std::env::var("TCLRS_JIT").as_deref(),
Ok("off") | Ok("0") | Ok("no")
)
})
}
fn interpreter_op(interp: &Shared, vm: &mut VM, id: u16, arg: u8) -> Result<(), TclError> {
match id {
ext::EVAL => eval_op(interp, vm, arg),
ext::FFI_CALL => ffi_op(vm, arg).map_err(TclError::plain),
ext::PROC_DEFINE => crate::procs::define_op(interp, vm),
ext::DYN_CALL => crate::procs::call_op(interp, vm, arg),
ext::EXPAND_CALL => crate::procs::expand_call_op(interp, vm, arg),
_ => extension(vm, id, arg).map_err(TclError::plain),
}
}
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 (filter, candidates) = if which == crate::cmd_info::SET_OF {
let pattern = to_tcl_string(&vm.pop());
let places = crate::list::split(&to_tcl_string(&vm.pop())).map_err(TclError::plain)?;
let names = crate::list::split(&to_tcl_string(&vm.pop())).map_err(TclError::plain)?;
(Some(pattern), Some((names, places)))
} else {
let given = matches!(vm.pop(), Value::Int(1));
let pattern = to_tcl_string(&vm.pop());
(given.then_some(pattern), None)
};
let mut names: Vec<String> = match which {
crate::cmd_info::COMMANDS => crate::names::commands()
.into_iter()
.map(|s| s.to_string())
.chain(chunk_procs(vm))
.collect(),
crate::cmd_info::PROCS => chunk_procs(vm).collect(),
crate::cmd_info::FRAME_LOCALS => {
let declared = interp.lock().expect("interpreter lock").frame_declared();
match declared {
None => Vec::new(),
Some(declared) => global_names_of(interp, vm)
.into_iter()
.filter(|n| !declared.iter().any(|d| d == n))
.collect(),
}
}
crate::cmd_info::SET_OF => {
let (names, places) = candidates.expect("SET_OF pushed its candidates");
names
.iter()
.zip(places.iter())
.filter(|(_, place)| match place.parse::<i64>() {
Ok(crate::cmd_info::ALWAYS) => true,
Ok(raw) => var_is_set(vm, Place::decode(raw)),
Err(_) => false,
})
.map(|(name, _)| name.clone())
.chain(
frame_of_current_level(vm)
.map(|frame| {
crate::cmd_scope::runtime_locals(vm, frame)
.into_iter()
.map(|(name, _)| name)
.collect::<Vec<String>>()
})
.unwrap_or_default(),
)
.collect()
}
_ => global_names_of(interp, vm),
};
let qualified = filter.as_deref().is_some_and(|p| p.contains("::"));
if let Some(p) = filter.as_deref() {
if qualified {
let p = absolute(p);
names.retain(|name| crate::list::glob_match(&p, &absolute(name)));
} else {
names.retain(|name| crate::list::glob_match(p, name));
}
}
if qualified {
for name in &mut names {
*name = absolute(name);
}
}
names.sort();
names.dedup();
vm.push(Value::Str(Arc::new(crate::list::join(&names))));
Ok(())
}
fn absolute(name: &str) -> String {
if name.starts_with("::") {
name.to_string()
} else {
format!("::{name}")
}
}
pub(crate) fn at_global<T, E>(
interp: &Shared,
vm: &mut VM,
body: impl FnOnce(&Shared) -> Result<T, E>,
) -> Result<T, E> {
flush(&vm.chunk, interp, &vm.globals);
let result = body(interp);
vm.globals = reproject(&vm.chunk, interp, &vm.globals);
result
}
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 = reproject(&vm.chunk, interp, &vm.globals);
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 takes_level = args.len() > 1 && crate::compiler::looks_like_a_level(&args[0]);
if args.len() == 1 && crate::compiler::looks_like_a_level(&args[0]) {
return Err(TclError::plain(
"wrong # args: should be \"uplevel ?level? command ?arg ...?\"".to_string(),
));
}
let level = if takes_level {
args.remove(0)
} else {
"1".to_string()
};
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)
}
pub(crate) 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 value = in_frame(interp, vm, up, declared, |interp| run_source(interp, src))?;
vm.push(value);
Ok(())
}
pub(crate) fn in_frame<T>(
interp: &Shared,
vm: &mut VM,
up: usize,
declared: &str,
body: impl FnOnce(&Shared) -> Result<T, TclError>,
) -> Result<T, TclError> {
let names: Vec<String> = vm.slot_names_at(up).to_vec();
let frame = match vm.frames.len().checked_sub(up + 1) {
Some(index) if names.is_empty() && vm.frames[index].entry_ip.is_none() => {
flush(&vm.chunk, interp, &vm.globals);
let result = body(interp);
vm.globals = seed(&vm.chunk, interp);
return result;
}
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);
}
}
}
for (name, value) in crate::cmd_scope::runtime_locals(vm, frame) {
view.insert(name, value);
}
{
let mut state = interp.lock().expect("interpreter lock");
state.globals = view;
state.projections.push(Projection {
outer,
declared: declared.clone(),
});
}
let result = body(interp);
let (after, mut outer) = {
let mut state = interp.lock().expect("interpreter lock");
let parked = state.projections.pop().expect("projection was pushed");
let after = std::mem::take(&mut state.globals);
(after, parked.outer)
};
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;
}
harvest_locals(vm, frame, &after, |name| {
!names.iter().any(|n| n == name) && !declared.iter().any(|n| n == name)
});
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);
result
}
fn harvest_locals(
vm: &mut VM,
frame: usize,
after: &HashMap<String, Value>,
grew: impl Fn(&str) -> bool,
) -> Vec<String> {
let known = crate::cmd_scope::runtime_names(vm, frame);
let mut fresh: Vec<String> = after
.keys()
.filter(|name| !known.iter().any(|n| n == *name) && grew(name))
.cloned()
.collect();
fresh.sort_unstable();
let taken: Vec<String> = known.into_iter().chain(fresh).collect();
for name in &taken {
let Some(slot) = crate::cmd_scope::runtime_slot_alloc(vm, frame, name) else {
continue;
};
let value = after.get(name).cloned().unwrap_or(Value::Undef);
vm.frames[frame].slots[usize::from(slot)] = value;
}
taken
}
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")
}
pub(crate) fn flush_globals(vm: &VM, interp: &Shared) {
flush(&vm.chunk, interp, &vm.globals);
}
pub(crate) fn reseed_globals(vm: &mut VM, interp: &Shared) {
vm.globals = reproject(&vm.chunk, interp, &vm.globals);
}
pub(crate) fn with_written_back<T>(
interp: &Shared,
vm: &mut VM,
body: impl FnOnce(&Shared) -> T,
) -> T {
flush_globals(vm, interp);
let out = body(interp);
reseed_globals(vm, interp);
out
}
pub(crate) fn global_value(interp: &Shared, name: &str) -> Option<Value> {
interp
.lock()
.expect("interpreter lock")
.globals
.get(crate::cmd_namespace::store_key(name))
.cloned()
}
pub(crate) fn global_names_of(interp: &Shared, vm: &VM) -> Vec<String> {
let mut names: Vec<String> = interp
.lock()
.expect("interpreter lock")
.globals
.keys()
.filter(|n| !n.starts_with('\u{0}'))
.cloned()
.collect();
let overflow = overflow_names(&vm.chunk, &vm.globals);
let named = vm.chunk.names.iter().enumerate().chain(
overflow
.iter()
.enumerate()
.map(|(offset, name)| (overflow_value_index(&vm.chunk, offset), name)),
);
for (slot, name) in named {
if name.starts_with('\u{0}') {
continue;
}
let name = crate::cmd_namespace::store_key(name);
if matches!(vm.globals.get(slot), Some(Value::Undef) | None) {
names.retain(|n| n != name);
continue;
}
names.push(name.to_string());
}
names
}
pub(crate) fn var_is_set(vm: &VM, place: Place) -> bool {
if let Place::Link(slot) = place {
let Some(link) = crate::cmd_scope::link_at(vm, slot) else {
return false;
};
return !matches!(
crate::cmd_scope::read_link(vm, &link),
Some(Value::Undef) | None
);
}
let value = match place {
Place::Global(index) => vm.globals.get(index as usize),
Place::Slot(slot) | Place::Link(slot) => {
vm.frames.last().and_then(|f| f.slots.get(slot as usize))
}
};
!matches!(value, Some(Value::Undef) | None)
}
struct VmScriptHost<'a> {
interp: &'a Shared,
vm: &'a mut VM,
}
impl crate::cmd_package::ScriptHost for VmScriptHost<'_> {
fn eval(&mut self, src: &str) -> Result<String, TclError> {
flush(&self.vm.chunk, self.interp, &self.vm.globals);
let result = run_source(self.interp, src);
self.vm.globals = reproject(&self.vm.chunk, self.interp, &self.vm.globals);
result.map(|v| to_tcl_string(&v))
}
}
fn package_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let (line, argv) = crate::cmd_package::take_args(vm, argc);
let outcome = {
let mut host = VmScriptHost { interp, vm };
crate::cmd_package::run(&argv, &mut host)
};
match outcome {
Ok(result) => {
vm.push(Value::Str(Arc::new(result)));
Ok(())
}
Err(e) => Err(TclError {
line: e.line.or(Some(line)),
..e
}),
}
}
#[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: Arc<Chunk>,
contexts: Vec<Context>,
live: HashMap<String, usize>,
created: HashSet<String>,
globals: Vec<Value>,
current: usize,
}
impl Machine {
fn run(shared: &Shared, chunk: Arc<Chunk>) -> Result<Value, TclError> {
Machine::start(shared, chunk, None)
}
fn start(
shared: &Shared,
chunk: Arc<Chunk>,
at: Option<(usize, Vec<Value>)>,
) -> Result<Value, TclError> {
let hooks = Hooks::new(Arc::clone(shared));
let mut main = acquire_vm(&chunk);
hooks.install(&mut main);
let globals = seed(&chunk, shared);
if let Some((entry, actuals)) = at {
let base = main.stack.len();
for value in actuals {
main.stack.push(value);
}
main.frames.push(Frame {
return_ip: chunk.ops.len(),
stack_base: base,
slots: Vec::new(),
entry_ip: Some(entry),
});
main.ip = entry;
}
shared
.lock()
.expect("interpreter lock")
.running
.push(Arc::clone(&chunk));
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();
if let Some(vm) = machine.contexts[0].vm.take() {
release_vm(&machine.chunk, vm);
}
flush(&machine.chunk, shared, &machine.globals);
shared.lock().expect("interpreter lock").running.pop();
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")
.contexts
.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")
.contexts
.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, mut e: TclError) -> Result<(), TclError> {
let mut depth = self.vm(self.current).frames.len();
loop {
if let Some(frame) = self.contexts[self.current].catches.last().copied() {
let vm = self.vm(self.current);
let raised_at = match vm.frames.get(frame.frames) {
Some(inner) => inner.return_ip.saturating_sub(1),
None => vm.ip,
};
for _ in 0..depth.saturating_sub(frame.frames) {
e = e.descend();
}
depth = frame.frames;
let code = e.visible_code();
let resume = match frame.kind {
FrameKind::Catch(handler) => Some(handler),
FrameKind::Loop { brk, .. } if code == TCL_BREAK => Some(brk),
FrameKind::Loop {
cont,
step_start,
step_end,
..
} if code == TCL_CONTINUE => {
(!(step_start..step_end).contains(&raised_at)).then_some(cont)
}
FrameKind::Loop { .. } => None,
};
let Some(resume) = resume else {
self.contexts[self.current].catches.pop();
continue;
};
if matches!(frame.kind, FrameKind::Catch(_)) {
self.contexts[self.current].catches.pop();
}
let options = e.options();
let vm = self.vm(self.current);
vm.frames.truncate(frame.frames);
vm.stack.truncate(frame.stack);
vm.stack.resize(frame.stack, Value::Undef);
if matches!(frame.kind, FrameKind::Catch(_)) {
vm.push(Value::Int(code as i64));
vm.push(Value::Str(Arc::new(options)));
vm.push(Value::Str(Arc::new(e.msg)));
}
vm.ip = resume;
return Ok(());
}
if e.level > 0 {
match self.spend_call_level(&mut e) {
Some(true) => return Ok(()),
Some(false) => {
depth = self.vm(self.current).frames.len();
continue;
}
None => {}
}
}
if self.current == 0 {
return Err(e);
}
match self.discard(self.current) {
Some(resumer) => self.current = resumer,
None => return Err(e),
}
}
}
fn spend_call_level(&mut self, e: &mut TclError) -> Option<bool> {
let vm = self.vm(self.current);
let mut activation = None;
while let Some(frame) = vm.frames.last() {
let frame = frame.clone();
if frame.entry_ip.is_some() {
activation = Some(frame);
break;
}
vm.frames.pop();
vm.stack.truncate(frame.stack_base);
}
let frame = activation?;
vm.frames.pop();
vm.stack.truncate(frame.stack_base);
*e = std::mem::replace(e, TclError::plain(String::new())).descend();
if e.visible_code() != TCL_OK {
return Some(false);
}
vm.stack
.push(Value::Str(Arc::new(std::mem::take(&mut e.msg))));
vm.ip = frame.return_ip;
Some(true)
}
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")
.contexts
.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 is_integral(&self) -> bool {
matches!(self, Num::Int(_) | Num::Big(_))
}
}
pub(crate) 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,
}
pub(crate) 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_integral() || q.is_integral() => 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 => {
for _ in 0..arg {
vm.pop();
}
Err(to_tcl_string(&vm.pop()))
}
ext::THROW => {
let message = to_tcl_string(&vm.pop());
let kind = to_tcl_string(&vm.pop());
match list::split(&kind) {
Err(e) => Err(e),
Ok(items) if items.is_empty() => Err("type must be non-empty list".to_string()),
Ok(_) => Err(message),
}
}
id if (ext::INFO_BASE..ext::INFO_END).contains(&id) => {
crate::cmd_info::extension(vm, id, arg)
}
id if crate::cmd_binary::is_op(id) => crate::cmd_binary::extension(vm, id, arg),
id if crate::cmd_encoding::is_op(id) => crate::cmd_encoding::extension(vm, id, arg),
id if id >= ext::FILE_BASE => crate::cmd_file::extension(vm, id, arg),
id if id >= ext::CLOCK_BASE => crate::cmd_clock::extension(vm, id, arg),
id if id >= ext::MATH_BASE => crate::expr_math::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 p.is_zero() {
return match q.is_negative() {
true => Err("exponentiation of zero by negative power".to_string()),
false => Ok(Value::Int(i64::from(q.is_zero()))),
};
}
if p == BigInt::from(1) {
return Ok(Value::Int(1));
}
if p == BigInt::from(-1) {
return Ok(Value::Int(match q.bit(0) {
true => -1,
false => 1,
}));
}
if q.is_negative() {
return 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 && (-1..=1).contains(&i) => {
Ok(Value::Int(match i {
0 => i64::from(j == 0),
1 => 1,
_ if j % 2 == 0 => 1,
_ => -1,
}))
}
(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])
}
Place::Link(slot) => {
let link = crate::cmd_scope::link_at(vm, slot)?;
crate::cmd_scope::write_link(vm, &link)
}
}
}
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 {
crate::cmd_list::forget_split();
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) if slot_form && *index < 0 => Ok(Place::Link((-index - 1) as u16)),
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);
pub(crate) 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);
#[derive(Clone, Debug, Default)]
pub(crate) struct ProcParams {
pub(crate) params: Vec<(String, Option<String>)>,
pub(crate) body: 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_body(vm: &VM, name: &str) -> Option<String> {
proc_params(vm, name)?.body
}
pub(crate) fn current_level(vm: &VM) -> i64 {
levels(vm).len() as i64
}
pub(crate) fn frame_of_level(vm: &VM, level: i64) -> Option<usize> {
let ups = levels(vm);
let out = usize::try_from(ups.len() as i64 - level).ok()?;
let up = *ups.get(out)?;
vm.frames.len().checked_sub(up + 1)
}
pub(crate) fn frame_of_current_level(vm: &VM) -> Option<usize> {
frame_of_level(vm, current_level(vm))
}
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")
}
}
#[cfg(test)]
mod numeric_hook_tests {
use super::*;
const L: i64 = 16_677_181_699_666_569;
fn truth(op: NumOp, a: Value, b: Value) -> i64 {
match numeric(op, &a, &b) {
Ok(Value::Int(i)) => i,
other => panic!("{op:?} answered {other:?}"),
}
}
#[test]
fn an_integer_past_two_to_the_fifty_third_orders_exactly_against_its_double() {
let (int, double) = (Value::Int(L), Value::Float(L as f64));
assert_eq!(double.clone(), Value::Float(16_677_181_699_666_568.0));
for (op, want) in [
(NumOp::Eq, 0),
(NumOp::Ne, 1),
(NumOp::Lt, 0),
(NumOp::Gt, 1),
(NumOp::Le, 0),
(NumOp::Ge, 1),
] {
assert_eq!(truth(op, int.clone(), double.clone()), want, "{op:?} L,D");
}
for (op, want) in [
(NumOp::Eq, 0),
(NumOp::Ne, 1),
(NumOp::Lt, 1),
(NumOp::Gt, 0),
(NumOp::Le, 1),
(NumOp::Ge, 0),
] {
assert_eq!(truth(op, double.clone(), int.clone()), want, "{op:?} D,L");
}
}
#[test]
fn the_boundary_and_its_negative_order_exactly_too() {
let m = 9_007_199_254_740_993i64; assert_eq!(truth(NumOp::Eq, Value::Int(m), Value::Float(m as f64)), 0);
assert_eq!(truth(NumOp::Gt, Value::Int(m), Value::Float(m as f64)), 1);
assert_eq!(truth(NumOp::Lt, Value::Int(-L), Value::Float(-L as f64)), 1);
assert_eq!(truth(NumOp::Gt, Value::Int(-L), Value::Float(-L as f64)), 0);
assert_eq!(truth(NumOp::Eq, Value::Int(-L), Value::Float(-L as f64)), 0);
}
#[test]
fn exactly_representable_and_double_only_pairs_are_unchanged() {
assert_eq!(truth(NumOp::Eq, Value::Int(3), Value::Float(3.0)), 1);
assert_eq!(truth(NumOp::Lt, Value::Int(2), Value::Float(2.5)), 1);
assert_eq!(truth(NumOp::Gt, Value::Int(3), Value::Float(2.5)), 1);
assert_eq!(truth(NumOp::Eq, Value::Float(1.5), Value::Float(1.5)), 1);
assert_eq!(truth(NumOp::Lt, Value::Float(1.5), Value::Float(2.5)), 1);
}
#[test]
fn nan_and_infinity_keep_their_answers() {
let nan = Value::Float(f64::NAN);
for op in [NumOp::Eq, NumOp::Lt, NumOp::Gt, NumOp::Le, NumOp::Ge] {
assert_eq!(truth(op, Value::Int(L), nan.clone()), 0, "{op:?} L,nan");
assert_eq!(truth(op, nan.clone(), Value::Int(L)), 0, "{op:?} nan,L");
}
assert_eq!(truth(NumOp::Ne, Value::Int(L), nan.clone()), 1);
assert_eq!(truth(NumOp::Ne, nan, Value::Int(L)), 1);
let inf = Value::Float(f64::INFINITY);
assert_eq!(truth(NumOp::Lt, Value::Int(L), inf.clone()), 1);
assert_eq!(
truth(NumOp::Gt, Value::Int(L), Value::Float(f64::NEG_INFINITY)),
1
);
assert_eq!(truth(NumOp::Gt, inf, Value::Int(L)), 1);
}
#[test]
fn arithmetic_still_promotes_the_integer_to_a_double() {
let (int, double) = (Value::Int(L), Value::Float(L as f64));
assert_eq!(
numeric(NumOp::Sub, &int, &double),
Ok(Value::Float(0.0)),
"subtraction promotes rather than answering the exact 1"
);
assert_eq!(
numeric(NumOp::Add, &int, &double),
Ok(Value::Float(33_354_363_399_333_136.0))
);
assert_eq!(
numeric(NumOp::Mul, &int, &Value::Float(1.0)),
Ok(Value::Float(16_677_181_699_666_568.0))
);
}
}