use std::collections::HashMap;
use std::sync::Mutex;
use fusevm::{Op, Value, VM};
use crate::compiler::{ext, CompileError, Compiler, Place, Scope};
use crate::parser::Word;
use crate::runtime::{to_tcl_string, Shared, TclError};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct BodySlots {
pub(crate) entry: usize,
pub(crate) end: usize,
pub(crate) names: Vec<(String, u16)>,
}
pub(crate) type SlotNames = Vec<BodySlots>;
static SLOT_NAMES: Mutex<Option<HashMap<u64, SlotNames>>> = Mutex::new(None);
pub(crate) fn note_slot_names(chunk: &fusevm::Chunk, bodies: &SlotNames) {
if bodies.is_empty() {
return;
}
let id = crate::runtime::chunk_identity(chunk);
let mut guard = SLOT_NAMES.lock().expect("slot names lock");
guard
.get_or_insert_with(HashMap::new)
.insert(id, bodies.clone());
}
fn body_at(id: u64, ip: usize) -> Option<BodySlots> {
let guard = SLOT_NAMES.lock().expect("slot names lock");
guard
.as_ref()?
.get(&id)?
.iter()
.filter(|b| (b.entry..b.end).contains(&ip))
.min_by_key(|b| b.end - b.entry)
.cloned()
}
pub(crate) fn frame_slots(vm: &VM, frame: usize) -> Option<BodySlots> {
if frame == 0 || frame >= vm.frames.len() {
return None;
}
let ip = match vm.frames.get(frame + 1) {
Some(inner) => inner.return_ip.checked_sub(1)?,
None => vm.ip.checked_sub(1)?,
};
body_at(crate::runtime::chunk_identity(&vm.chunk), ip)
}
const ROSTER_TAG: &str = "\u{0}roster";
fn roster_base(vm: &VM, frame: usize) -> Option<usize> {
let up = vm.frames.len().checked_sub(frame + 1)?;
vm.frames.get(frame)?.entry_ip?;
Some(vm.slot_names_at(up).len())
}
pub(crate) fn runtime_names(vm: &VM, frame: usize) -> Vec<String> {
let Some(base) = roster_base(vm, frame) else {
return Vec::new();
};
match vm.frames[frame].slots.get(base) {
Some(Value::Array(items)) => match items.split_first() {
Some((tag, names)) if to_tcl_string(tag) == ROSTER_TAG => {
names.iter().map(to_tcl_string).collect()
}
_ => Vec::new(),
},
_ => Vec::new(),
}
}
pub(crate) fn runtime_slot(vm: &VM, frame: usize, name: &str) -> Option<u16> {
let base = roster_base(vm, frame)?;
let at = runtime_names(vm, frame).iter().position(|n| n == name)?;
u16::try_from(base + 1 + at).ok()
}
pub(crate) fn runtime_slot_alloc(vm: &mut VM, frame: usize, name: &str) -> Option<u16> {
if let Some(slot) = runtime_slot(vm, frame, name) {
return Some(slot);
}
let base = roster_base(vm, frame)?;
let mut names = runtime_names(vm, frame);
names.push(name.to_string());
let at = names.len() - 1;
let slot = u16::try_from(base + 1 + at).ok()?;
let mut roster = Vec::with_capacity(names.len() + 1);
roster.push(Value::Str(std::sync::Arc::new(ROSTER_TAG.to_string())));
for n in names {
roster.push(Value::Str(std::sync::Arc::new(n)));
}
let slots = &mut vm.frames[frame].slots;
if slots.len() <= usize::from(slot) {
slots.resize(usize::from(slot) + 1, Value::Undef);
}
slots[base] = Value::array(roster);
Some(slot)
}
pub(crate) fn runtime_locals(vm: &VM, frame: usize) -> Vec<(String, Value)> {
runtime_names(vm, frame)
.into_iter()
.filter_map(|name| {
let slot = runtime_slot(vm, frame, &name)?;
match vm.frames[frame].slots.get(usize::from(slot)) {
Some(v) if *v != Value::Undef => Some((name, v.clone())),
_ => None,
}
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Link {
pub(crate) home: Home,
pub(crate) elem: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Home {
Global(u16),
Slot { frame: u16, slot: u16 },
}
const LINK_TAG: &str = "\u{0}upvar";
impl Link {
pub(crate) fn encode(&self) -> Value {
let mut parts = vec![Value::Str(std::sync::Arc::new(LINK_TAG.to_string()))];
match self.home {
Home::Global(idx) => {
parts.push(Value::Int(0));
parts.push(Value::Int(i64::from(idx)));
}
Home::Slot { frame, slot } => {
parts.push(Value::Int(1));
parts.push(Value::Int(i64::from(frame)));
parts.push(Value::Int(i64::from(slot)));
}
}
if let Some(key) = &self.elem {
parts.push(Value::Str(std::sync::Arc::new(key.clone())));
}
Value::array(parts)
}
pub(crate) fn decode(value: &Value) -> Option<Link> {
let Value::Array(parts) = value else {
return None;
};
if !matches!(parts.first(), Some(v) if to_tcl_string(v) == LINK_TAG) {
return None;
}
let int = |i: usize| match parts.get(i) {
Some(Value::Int(n)) => Some(*n),
_ => None,
};
let (home, next) = match int(1)? {
0 => (Home::Global(int(2)? as u16), 3),
1 => (
Home::Slot {
frame: int(2)? as u16,
slot: int(3)? as u16,
},
4,
),
_ => return None,
};
Some(Link {
home,
elem: parts.get(next).map(to_tcl_string),
})
}
}
pub(crate) fn link_at(vm: &VM, slot: u16) -> Option<Link> {
let frame = vm.frames.last()?;
Link::decode(frame.slots.get(usize::from(slot))?)
}
impl Compiler {
pub(crate) fn publish_slot_names(&mut self, scope: &Scope, entry: usize, end: usize) {
let mut names: Vec<(String, u16)> = scope
.locals
.iter()
.chain(scope.links.iter())
.map(|(name, slot)| (name.clone(), *slot))
.collect();
names.sort_by_key(|(_, slot)| *slot);
self.slot_names.push(BodySlots { entry, end, names });
}
pub(crate) fn cmd_upvar(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.len() < 2 {
return self.error(
"wrong # args: should be \"upvar ?level? otherVar localVar ?otherVar localVar ...?\"",
);
}
let has_level = !args.len().is_multiple_of(2);
let (level, pairs) = if has_level {
(Some(&args[0]), &args[1..])
} else {
(None, args)
};
if pairs.is_empty() {
return self.error(
"wrong # args: should be \"upvar ?level? otherVar localVar ?otherVar localVar ...?\"",
);
}
let literal_global = level
.and_then(|w| w.as_literal())
.is_some_and(|text| parse_level(text) == Some(Level::Absolute(0)));
if literal_global {
let literal_pairs: Option<Vec<(String, String)>> = pairs
.chunks(2)
.map(|p| {
let other = crate::assoc::target_of(&p[0])?;
let local = crate::assoc::target_of(&p[1])?;
match (other, local) {
(
crate::assoc::Target::Scalar(other),
crate::assoc::Target::Scalar(local),
) => Some((other, local)),
_ => None,
}
})
.collect();
if let Some(bound) = literal_pairs {
for (other, local) in &bound {
self.bind_alias(local, other)?;
}
if self.scope.is_some() {
self.push_empty();
return Ok(());
}
for (other, local) in &bound {
self.emit(Op::LoadInt(NO_SLOT), 1);
self.push_str(local);
self.push_str("#0");
self.push_str(other);
self.emit(Op::Extended(ext::UPVAR, 4), -3);
self.emit(Op::Pop, -1);
}
self.push_empty();
return Ok(());
}
}
let mut locals = Vec::new();
for pair in pairs.chunks(2) {
let local = self.var_name_of(&pair[1])?;
if local.contains("::") {
return self.error(format!(
"bad variable name \"{local}\": can't create namespace variable that refers \
to procedure variable"
));
}
let slot = match self.scope {
Some(_) => i64::from(self.link_slot(&local)?),
None => NO_SLOT,
};
locals.push((slot, local));
}
for (slot, local) in &locals {
self.emit(Op::LoadInt(*slot), 1);
self.push_str(local);
}
match level {
Some(w) => self.word(w)?,
None => self.push_empty(),
}
for pair in pairs.chunks(2) {
self.word(&pair[0])?;
}
let pushed = locals.len() * 3 + 1;
let count = u8::try_from(pushed)
.map_err(|_| self.err("too many arguments for \"upvar\"".to_string()))?;
self.emit(Op::Extended(ext::UPVAR, count), 1 - pushed as i32);
Ok(())
}
fn link_slot(&mut self, name: &str) -> Result<u16, CompileError> {
let Some(scope) = self.scope.as_mut() else {
return self.error("\"upvar\" outside a procedure has no local scope");
};
if let Some(slot) = scope.links.get(name) {
return Ok(*slot);
}
if scope.locals.contains_key(name) || scope.aliases.contains_key(name) {
return self.error(format!("variable \"{name}\" already exists"));
}
let slot = scope.next_slot;
scope.next_slot += 1;
scope.links.insert(name.to_string(), slot);
Ok(slot)
}
fn bind_alias(&mut self, local: &str, other: &str) -> Result<(), CompileError> {
let Some(scope) = self.scope.as_mut() else {
let local = crate::cmd_namespace::global_key(self, local);
let other = crate::cmd_namespace::global_key(self, other);
let local = crate::cmd_namespace::store_key(&local).to_string();
let other = crate::cmd_namespace::store_key(&other).to_string();
if local == other {
return self.error("can't upvar from variable to itself");
}
self.top_aliases.insert(local, other);
return Ok(());
};
if scope.locals.contains_key(local) || scope.links.contains_key(local) {
return Err(CompileError {
msg: format!("variable \"{local}\" already exists"),
line: self.command_line,
});
}
scope.aliases.insert(local.to_string(), other.to_string());
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Level {
Absolute(i64),
Up(i64),
}
fn parse_level(text: &str) -> Option<Level> {
if let Some(digits) = text.strip_prefix('#') {
return digits.parse::<i64>().ok().map(Level::Absolute);
}
text.parse::<i64>().ok().map(Level::Up)
}
fn absolute_level(text: &str, current: i64) -> Option<i64> {
match parse_level(text)? {
Level::Absolute(n) if n < 0 => None,
Level::Absolute(n) => Some(n),
Level::Up(n) => Some(current - n),
}
}
pub(crate) fn upvar_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let pairs = (usize::from(argc) - 1) / 3;
let mut others: Vec<String> = (0..pairs).map(|_| to_tcl_string(&vm.pop())).collect();
others.reverse();
let level_word = to_tcl_string(&vm.pop());
let mut locals: Vec<(i64, String)> = (0..pairs)
.map(|_| {
let local = to_tcl_string(&vm.pop());
(pop_int(vm), local)
})
.collect();
locals.reverse();
let current = crate::runtime::current_level(vm);
let target = if level_word.is_empty() {
current - 1
} else {
match absolute_level(&level_word, current) {
Some(n) => n,
None => return Err(TclError::plain(format!("bad level \"{level_word}\""))),
}
};
if target < 0 || target > current {
let named = if level_word.is_empty() {
"1".to_string()
} else {
level_word
};
return Err(TclError::plain(format!("bad level \"{named}\"")));
}
for ((slot, local), other) in locals.iter().zip(others.iter()) {
if *slot == NO_SLOT {
let (base, elem) = split_element(other);
if elem.is_some() {
return Err(TclError::plain(format!(
"\"upvar\" outside a procedure to the array element \"{other}\" is not \
supported: an alias between two entries of the interpreter's variable table \
cannot name one element of one of them"
)));
}
crate::runtime::alias_global(interp, local, &base).map_err(TclError::plain)?;
continue;
}
let link = resolve_target(interp, vm, target, other)?;
let cell = crate::runtime::var_cell(vm, Place::Slot(*slot as u16))
.ok_or_else(|| TclError::plain("\"upvar\" outside a procedure"))?;
*cell = link.encode();
}
if locals.iter().any(|(slot, _)| *slot == NO_SLOT) {
crate::runtime::flush_globals(vm, interp);
crate::runtime::reseed_globals(vm, interp);
}
vm.push(Value::Str(std::sync::Arc::new(String::new())));
Ok(())
}
const NO_SLOT: i64 = -1;
fn pop_int(vm: &mut VM) -> i64 {
match vm.pop() {
Value::Int(n) => n,
other => to_tcl_string(&other).parse().unwrap_or(0),
}
}
fn resolve_target(
interp: &Shared,
vm: &mut VM,
target: i64,
other: &str,
) -> Result<Link, TclError> {
let (base, elem) = split_element(other);
let qualified = base.contains("::");
let home = if target == 0 || qualified {
Home::Global(global_home(interp, vm, &base)?)
} else {
frame_home(vm, target, &base)?
};
let link = link_of_home(vm, &Link { home, elem: elem.clone() })
.unwrap_or(Link { home, elem });
if link.elem.is_some() {
materialize_array(vm, &link, other)?;
}
Ok(link)
}
fn split_element(name: &str) -> (String, Option<String>) {
match name.find('(') {
Some(open) if name.ends_with(')') => (
name[..open].to_string(),
Some(name[open + 1..name.len() - 1].to_string()),
),
_ => (name.to_string(), None),
}
}
fn global_home(interp: &Shared, vm: &mut VM, name: &str) -> Result<u16, TclError> {
let key = crate::cmd_namespace::store_key(name).to_string();
if let Some(idx) = vm
.chunk
.names
.iter()
.position(|n| crate::cmd_namespace::store_key(n) == key)
{
return u16::try_from(idx).map_err(|_| {
TclError::plain(format!("\"upvar\" cannot reach the variable \"{name}\""))
});
}
crate::runtime::intern_overflow(interp, vm, &key)
.map_err(|msg| TclError::plain(format!("\"upvar\" cannot reach \"{name}\": {msg}")))
}
fn frame_home(vm: &mut VM, target: i64, name: &str) -> Result<Home, TclError> {
if let Some(home) = frame_home_opt(vm, target, name)? {
return Ok(home);
}
let Some(frame) = crate::runtime::frame_of_level(vm, target) else {
return Err(TclError::plain(format!("bad level \"{target}\"")));
};
match runtime_slot_alloc(vm, frame, name) {
Some(slot) => Ok(Home::Slot {
frame: u16::try_from(frame).map_err(|_| TclError::plain("call stack too deep"))?,
slot,
}),
None => Err(TclError::plain(format!(
"\"upvar\" to the variable \"{name}\" at level {target} is not supported: there is \
no procedure activation at that level to hold it"
))),
}
}
fn frame_home_opt(vm: &VM, target: i64, name: &str) -> Result<Option<Home>, TclError> {
let Some(frame) = crate::runtime::frame_of_level(vm, target) else {
return Err(TclError::plain(format!("bad level \"{target}\"")));
};
let Some(body) = frame_slots(vm, frame) else {
return Err(TclError::plain(format!(
"\"upvar\" to level {target} is not supported here: no slot names were recorded for \
the procedure running at that level"
)));
};
let slot = body
.names
.iter()
.find(|(n, _)| n == name)
.map(|(_, slot)| *slot);
match slot {
Some(slot) => Ok(Some(Home::Slot {
frame: u16::try_from(frame).map_err(|_| TclError::plain("call stack too deep"))?,
slot,
})),
None => Ok(None),
}
}
pub(crate) fn dict_with_home(
interp: &Shared,
vm: &mut VM,
key: &str,
) -> Result<Option<Link>, TclError> {
let (base, elem) = split_element(key);
let level = crate::runtime::current_level(vm);
if level == 0 || base.contains("::") {
let home = Home::Global(global_home(interp, vm, &base)?);
return Ok(Some(Link { home, elem }));
}
if let Some(home) = frame_home_opt(vm, level, &base)? {
let link = Link { home, elem };
return Ok(Some(link_of_home(vm, &link).unwrap_or(link)));
}
let Some(frame) = crate::runtime::frame_of_level(vm, level) else {
return Ok(None);
};
Ok(runtime_slot_alloc(vm, frame, &base).map(|slot| Link {
home: Home::Slot {
frame: u16::try_from(frame).unwrap_or(u16::MAX),
slot,
},
elem,
}))
}
pub(crate) fn link_get(vm: &mut VM, tolerant: bool) -> Result<(), TclError> {
let slot = pop_slot(vm);
let name = to_tcl_string(&vm.pop());
let Some(link) = link_at(vm, slot) else {
return Err(TclError::plain(format!(
"can't read \"{name}\": no such variable"
)));
};
match read_link(vm, &link).cloned() {
Some(v) if v != Value::Undef => vm.push(v),
_ if tolerant => vm.push(Value::Int(0)),
_ => {
return Err(TclError::plain(format!(
"can't read \"{name}\": no such variable"
)))
}
}
Ok(())
}
pub(crate) fn link_set(vm: &mut VM) -> Result<(), TclError> {
let slot = pop_slot(vm);
let name = to_tcl_string(&vm.pop());
let value = vm.pop();
let Some(link) = link_at(vm, slot) else {
return Err(TclError::plain(format!(
"can't set \"{name}\": no such variable"
)));
};
let Some(cell) = write_link(vm, &link) else {
return Err(TclError::plain(format!(
"can't set \"{name}\": variable isn't array"
)));
};
*cell = value;
Ok(())
}
fn pop_slot(vm: &mut VM) -> u16 {
match vm.pop() {
Value::Int(n) => n as u16,
other => to_tcl_string(&other).parse().unwrap_or(0),
}
}
pub(crate) fn dynamic_link(
interp: &Shared,
vm: &mut VM,
name: &str,
declared: &str,
) -> Result<Link, TclError> {
let (base, elem) = split_element(name);
let level = crate::runtime::current_level(vm);
let is_global = level == 0
|| base.contains("::")
|| crate::list::split(declared).is_ok_and(|names| names.iter().any(|n| *n == base));
if is_global {
let home = Home::Global(global_home(interp, vm, &base)?);
return Ok(Link { home, elem });
}
let home = match frame_home_opt(vm, level, &base)? {
Some(home) => home,
None => {
let Some(frame) = crate::runtime::frame_of_level(vm, level) else {
return Err(TclError::plain(format!(
"can't resolve \"{name}\": no variable context here"
)));
};
let slot = runtime_slot_alloc(vm, frame, &base).ok_or_else(|| {
TclError::plain(format!(
"can't resolve \"{name}\": there is no procedure activation to hold it"
))
})?;
Home::Slot {
frame: u16::try_from(frame).map_err(|_| TclError::plain("call stack too deep"))?,
slot,
}
}
};
let link = Link { home, elem };
Ok(link_of_home(vm, &link).unwrap_or(link))
}
pub(crate) fn dyn_get_op(interp: &Shared, vm: &mut VM, absent: u8) -> Result<(), TclError> {
let name = to_tcl_string(&vm.pop());
let declared = to_tcl_string(&vm.pop());
let link = dynamic_link(interp, vm, &name, &declared)?;
let missing = |absent: u8| match absent {
1 => Ok(Value::Int(0)),
2 => Ok(Value::Str(std::sync::Arc::new(String::new()))),
_ => Err(TclError::plain(format!(
"can't read \"{name}\": no such variable"
))),
};
let base = read_link(
vm,
&Link {
home: link.home,
elem: None,
},
)
.cloned();
let value = match (&link.elem, base) {
(None, Some(Value::Hash(_))) => {
return Err(TclError::plain(format!(
"can't read \"{name}\": variable is array"
)))
}
(None, Some(v)) if v != Value::Undef => v,
(None, _) => missing(absent)?,
(Some(key), Some(Value::Hash(map))) => match map.get(key) {
Some(v) if *v != Value::Undef => v.clone(),
_ if absent != 0 => missing(absent)?,
_ => {
return Err(TclError::plain(format!(
"can't read \"{name}\": no such element in array"
)))
}
},
(Some(_), Some(Value::Undef) | None) => missing(absent)?,
(Some(_), Some(_)) if absent == 2 => Value::Str(std::sync::Arc::new(String::new())),
(Some(_), Some(_)) => {
return Err(TclError::plain(format!(
"can't read \"{name}\": variable isn't array"
)))
}
};
vm.push(value);
Ok(())
}
pub(crate) fn dyn_set_op(interp: &Shared, vm: &mut VM) -> Result<(), TclError> {
let name = to_tcl_string(&vm.pop());
let declared = to_tcl_string(&vm.pop());
let value = vm.pop();
let link = dynamic_link(interp, vm, &name, &declared)?;
if link.elem.is_none() {
if let Some(Value::Hash(_)) = read_link(vm, &link) {
return Err(TclError::plain(format!(
"can't set \"{name}\": variable is array"
)));
}
}
let Some(cell) = write_link(vm, &link) else {
return Err(TclError::plain(format!(
"can't set \"{name}\": variable isn't array"
)));
};
*cell = value;
Ok(())
}
pub(crate) fn dyn_unset_op(interp: &Shared, vm: &mut VM, complain: bool) -> Result<(), TclError> {
let name = to_tcl_string(&vm.pop());
let declared = to_tcl_string(&vm.pop());
let link = dynamic_link(interp, vm, &name, &declared)?;
let existed = match &link.elem {
None => match write_link(vm, &link) {
Some(cell) if *cell != Value::Undef => {
*cell = Value::Undef;
true
}
_ => false,
},
Some(key) => {
let key = key.clone();
match base_cell(vm, &link) {
Some(Value::Hash(map)) => {
if map.remove(&key).is_some() {
true
} else {
return if complain {
Err(TclError::plain(format!(
"can't unset \"{name}\": no such element in array"
)))
} else {
Ok(())
};
}
}
Some(Value::Undef) | None => false,
Some(_) => {
return if complain {
Err(TclError::plain(format!(
"can't unset \"{name}\": variable isn't array"
)))
} else {
Ok(())
}
}
}
}
};
if complain && !existed {
return Err(TclError::plain(format!(
"can't unset \"{name}\": no such variable"
)));
}
Ok(())
}
pub(crate) fn dyn_exists_op(vm: &mut VM, interp: &Shared) -> Result<(), TclError> {
let name = to_tcl_string(&vm.pop());
let declared = to_tcl_string(&vm.pop());
let (base, elem) = split_element(&name);
let level = crate::runtime::current_level(vm);
let is_global = level == 0
|| base.contains("::")
|| crate::list::split(&declared).is_ok_and(|names| names.iter().any(|n| *n == base));
let home = if is_global {
let key = crate::cmd_namespace::store_key(&base).to_string();
match vm
.chunk
.names
.iter()
.position(|n| crate::cmd_namespace::store_key(n) == key)
.and_then(|idx| u16::try_from(idx).ok())
{
Some(idx) => Some(Home::Global(idx)),
None => {
let held = crate::runtime::global_value(interp, &key);
let set = match (&held, &elem) {
(None, _) | (Some(Value::Undef), _) => false,
(Some(Value::Hash(map)), Some(key)) => {
matches!(map.get(key), Some(v) if *v != Value::Undef)
}
(Some(_), Some(_)) => false,
(Some(_), None) => true,
};
vm.push(Value::Int(i64::from(set)));
return Ok(());
}
}
} else {
match frame_home_opt(vm, level, &base)? {
Some(home) => Some(home),
None => crate::runtime::frame_of_level(vm, level)
.and_then(|frame| {
runtime_slot(vm, frame, &base).map(|slot| (frame, slot))
})
.and_then(|(frame, slot)| {
Some(Home::Slot {
frame: u16::try_from(frame).ok()?,
slot,
})
}),
}
};
let set = match home {
None => false,
Some(home) => {
let link = Link { home, elem };
let link = match link_of_home(vm, &link) {
Some(followed) => followed,
None => link,
};
matches!(read_link(vm, &link), Some(v) if *v != Value::Undef)
}
};
vm.push(Value::Int(i64::from(set)));
Ok(())
}
fn link_of_home(vm: &VM, link: &Link) -> Option<Link> {
let Home::Slot { frame, slot } = link.home else {
return None;
};
let inner = vm
.frames
.get(usize::from(frame))
.and_then(|f| f.slots.get(usize::from(slot)))
.and_then(Link::decode)?;
Some(Link {
home: inner.home,
elem: link.elem.clone().or(inner.elem),
})
}
fn home_place(link: &Link) -> Place {
match link.home {
Home::Global(idx) => Place::Global(idx),
Home::Slot { slot, .. } => Place::Slot(slot),
}
}
pub(crate) fn read_link<'v>(vm: &'v VM, link: &Link) -> Option<&'v Value> {
let base = match link.home {
Home::Global(idx) => vm.globals.get(usize::from(idx)),
Home::Slot { frame, slot } => vm
.frames
.get(usize::from(frame))
.and_then(|f| f.slots.get(usize::from(slot))),
}?;
match &link.elem {
None => Some(base),
Some(key) => match base {
Value::Hash(map) => map.get(key),
_ => None,
},
}
}
pub(crate) fn unset_link(vm: &mut VM, slot: u16) -> bool {
let Some(link) = link_at(vm, slot) else {
return false;
};
let Some(key) = link.elem.clone() else {
return match write_link(vm, &link) {
Some(cell) if *cell != Value::Undef => {
*cell = Value::Undef;
true
}
_ => false,
};
};
let base = match link.home {
Home::Global(idx) => vm.globals.get_mut(usize::from(idx)),
Home::Slot { frame, slot } => vm
.frames
.get_mut(usize::from(frame))
.and_then(|f| f.slots.get_mut(usize::from(slot))),
};
match base {
Some(Value::Hash(map)) => map.remove(&key).is_some(),
_ => false,
}
}
fn base_cell<'v>(vm: &'v mut VM, link: &Link) -> Option<&'v mut Value> {
match link.home {
Home::Global(_) => crate::runtime::var_cell(vm, home_place(link)),
Home::Slot { frame, slot } => {
let f = vm.frames.get_mut(usize::from(frame))?;
let slot = usize::from(slot);
if slot >= f.slots.len() {
f.slots.resize(slot + 1, Value::Undef);
}
Some(&mut f.slots[slot])
}
}
}
fn materialize_array(vm: &mut VM, link: &Link, spelled: &str) -> Result<(), TclError> {
let Some(base) = base_cell(vm, link) else {
return Ok(());
};
match base {
Value::Undef => {
*base = Value::Hash(HashMap::new());
Ok(())
}
Value::Hash(_) => Ok(()),
_ => Err(TclError::plain(format!(
"can't access \"{spelled}\": variable isn't array"
))),
}
}
pub(crate) fn write_link<'v>(vm: &'v mut VM, link: &Link) -> Option<&'v mut Value> {
let base = base_cell(vm, link)?;
let Some(key) = &link.elem else {
return Some(base);
};
if *base == Value::Undef {
*base = Value::Hash(HashMap::new());
}
match base {
Value::Hash(map) => Some(map.entry(key.clone()).or_insert(Value::Undef)),
_ => None,
}
}
pub(crate) type Aliases = HashMap<String, String>;
pub(crate) type Links = HashMap<String, u16>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_level_word_is_told_apart_from_a_script() {
assert_eq!(parse_level("#0"), Some(Level::Absolute(0)));
assert_eq!(parse_level("#3"), Some(Level::Absolute(3)));
assert_eq!(parse_level("1"), Some(Level::Up(1)));
assert_eq!(parse_level("-1"), Some(Level::Up(-1)));
assert_eq!(parse_level("set x 1"), None);
assert_eq!(parse_level("#"), None);
assert_eq!(parse_level(""), None);
}
#[test]
fn a_level_word_resolves_to_an_absolute_level() {
assert_eq!(absolute_level("#0", 3), Some(0));
assert_eq!(absolute_level("#2", 3), Some(2));
assert_eq!(absolute_level("1", 3), Some(2));
assert_eq!(absolute_level("0", 3), Some(3));
assert_eq!(absolute_level("3", 3), Some(0));
assert_eq!(absolute_level("#-1", 3), None);
assert_eq!(absolute_level("nope", 3), None);
}
#[test]
fn an_element_target_splits_on_the_first_paren() {
assert_eq!(
split_element("a(i)"),
("a".to_string(), Some("i".to_string()))
);
assert_eq!(
split_element("p(a(b))"),
("p".to_string(), Some("a(b)".to_string()))
);
assert_eq!(split_element("q(x)y"), ("q(x)y".to_string(), None));
assert_eq!(split_element("plain"), ("plain".to_string(), None));
}
#[test]
fn a_link_round_trips_through_the_slot_it_lives_in() {
for link in [
Link {
home: Home::Global(7),
elem: None,
},
Link {
home: Home::Global(7),
elem: Some("k".to_string()),
},
Link {
home: Home::Slot { frame: 2, slot: 5 },
elem: None,
},
Link {
home: Home::Slot { frame: 2, slot: 5 },
elem: Some("i j".to_string()),
},
] {
assert_eq!(Link::decode(&link.encode()), Some(link));
}
assert_eq!(Link::decode(&Value::Int(3)), None);
assert_eq!(Link::decode(&Value::array(vec![Value::Int(0)])), None);
}
#[test]
fn a_place_round_trips_through_its_operand() {
for place in [
Place::Global(0),
Place::Global(65535),
Place::Slot(0),
Place::Slot(65535),
Place::Link(0),
Place::Link(65535),
] {
assert_eq!(Place::decode(place.encode()), place);
}
}
#[test]
fn a_lambda_scope_matches_the_prologue_it_is_emitted_with() {
let sig = crate::procs::parse_signature("l", "a {b 2} args").expect("a valid signature");
let scope = crate::procs::scope_for(&sig);
assert_eq!(scope.locals.get("a"), Some(&0));
assert_eq!(scope.locals.get("b"), Some(&1));
assert_eq!(scope.locals.get("args"), Some(&2));
assert_eq!(scope.next_slot, 3);
}
#[test]
fn a_lambda_uses_the_procedure_signature_parser() {
let sig: crate::procs::Signature =
crate::procs::parse_signature("l", "x").expect("a valid signature");
assert_eq!(sig.params.len(), 1);
let first = &sig.params[0];
assert_eq!(first.name, "x");
assert_eq!(first.default, None);
}
}