use std::cell::RefCell;
use std::sync::Arc;
use fusevm::{Op, Value, VM};
use crate::assoc::Target;
use crate::compiler::{ext, CompileError, Compiler, Place};
use crate::list;
use crate::parser::Word;
use crate::runtime::{place_at, place_of, take_var, to_tcl_string, var_cell, Shared};
pub const COMMANDS: &[&str] = &[
"list", "llength", "lindex", "lrange", "lreverse", "linsert", "lreplace", "lsearch", "lsort",
"join", "split", "concat", "lappend", "lassign", "lset", "lpop", "ledit", "lrepeat", "lremove",
"lseq", "lmap",
];
pub(crate) fn compile(c: &mut Compiler, name: &str, args: &[Word]) -> Result<(), CompileError> {
let (id, usage, min, max) = match name {
"list" => (ext::LIST, "list ?arg ...?", 0, usize::MAX),
"llength" => (ext::LLENGTH, "llength list", 1, 1),
"lindex" => (ext::LINDEX, "lindex list ?index ...?", 1, usize::MAX),
"lrange" => (ext::LRANGE, "lrange list first last", 3, 3),
"lreverse" => (ext::LREVERSE, "lreverse list", 1, 1),
"linsert" => (
ext::LINSERT,
"linsert list index ?element ...?",
2,
usize::MAX,
),
"lreplace" => (
ext::LREPLACE,
"lreplace list first last ?element ...?",
3,
usize::MAX,
),
"lsearch" => (
ext::LSEARCH,
"lsearch ?-option value ...? list pattern",
2,
usize::MAX,
),
"lsort" => (ext::LSORT, "lsort ?-option value ...? list", 1, usize::MAX),
"join" => (ext::JOIN, "join list ?joinString?", 1, 2),
"split" => (ext::SPLIT, "split string ?splitChars?", 1, 2),
"concat" => (ext::CONCAT, "concat ?arg ...?", 0, usize::MAX),
"lrepeat" => (ext::LREPEAT, "lrepeat count ?value ...?", 1, usize::MAX),
"lremove" => (ext::LREMOVE, "lremove list ?index ...?", 1, usize::MAX),
"lseq" => (ext::LSEQ, "lseq n ??op? n ??by? n??", 0, usize::MAX),
"lappend" => return lappend(c, args),
"lassign" => return lassign(c, args),
"lset" => return lset(c, args),
"lpop" => return lpop(c, args),
"ledit" => return ledit(c, args),
"lmap" => return lmap(c, args),
other => return c.error(format!("invalid command name \"{other}\"")),
};
if args.len() < min || args.len() > max {
return c.error(format!("wrong # args: should be \"{usage}\""));
}
let count = arg_count(c, args.len())?;
for arg in args {
c.word(arg)?;
}
c.emit(Op::Extended(id, count), 1 - args.len() as i32);
Ok(())
}
fn lappend(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let Some((name, values)) = args.split_first() else {
return c.error("wrong # args: should be \"lappend varName ?value ...?\"");
};
if crate::assoc::target_of(name).is_none() {
let count = arg_count(c, values.len() + 1)?;
c.dyn_read_modify(name, crate::compiler::Absent::Empty)?;
for value in values {
c.word(value)?;
}
c.emit(Op::Extended(ext::LAPPEND, count), -(values.len() as i32));
c.dyn_write_back();
return Ok(());
}
if let Target::Elem { name, index } = c.target_of(name)? {
let count = arg_count(c, values.len() + 1)?;
c.elem_get_tolerant(&name, &index)?;
for value in values {
c.word(value)?;
}
c.emit(Op::Extended(ext::LAPPEND, count), -(values.len() as i32));
return c.elem_store(&name, &index);
}
let name = c.var_name_of(name)?;
let count = arg_count(c, values.len() + 1)?;
if c.is_array(&name) {
c.emit_get_var(&name);
for value in values {
c.word(value)?;
}
c.emit(Op::Extended(ext::LAPPEND, count), -(values.len() as i32));
c.emit(Op::Dup, 1);
c.emit_set_var(&name);
return Ok(());
}
let place = c.var_place(&name);
let id = if place.in_frame() {
ext::LAPPEND_SLOT
} else {
ext::LAPPEND_VAR
};
c.emit(Op::LoadInt(place.frame_operand()), 1);
for value in values {
c.word(value)?;
}
c.emit(Op::Extended(id, count), -(values.len() as i32));
Ok(())
}
fn lassign(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let Some((list, vars)) = args.split_first() else {
return c.error("wrong # args: should be \"lassign list ?varName ...?\"");
};
let targets: Vec<Target> = vars
.iter()
.map(|w| c.target_of(w))
.collect::<Result<_, _>>()?;
c.word(list)?;
let count = arg_count(c, targets.len())?;
c.emit(Op::Extended(ext::LASSIGN, count), targets.len() as i32);
for target in &targets {
match target {
Target::Scalar(name) => c.emit_set_var(name),
Target::Elem { name, index } => {
c.elem_store(name, index)?;
c.emit(Op::Pop, -1);
}
}
}
Ok(())
}
fn var_target(c: &mut Compiler, target: &Target) -> Result<usize, CompileError> {
match target {
Target::Scalar(name) => {
c.push_str(name);
let place = c.var_place(name);
c.push_value(Value::Int(i64::from(place.in_frame())));
c.emit(Op::LoadInt(place.frame_operand()), 1);
c.push_str("");
c.push_value(Value::Int(0));
}
Target::Elem { name, index } => {
let place = c.array_place_of(name);
c.push_str(&Compiler::elem_report_name(name, index));
c.push_value(Value::Int(i64::from(place.in_frame())));
c.emit(Op::LoadInt(place.frame_operand()), 1);
c.index_value(index)?;
c.push_value(Value::Int(1));
}
}
Ok(4)
}
fn lset(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
const USAGE: &str = "wrong # args: should be \"lset listVar ?index? ?index ...? value\"";
if args.len() < 2 {
return c.error(USAGE);
}
let target = c.target_of(&args[0])?;
let operands = var_target(c, &target)?;
for arg in &args[1..] {
c.word(arg)?;
}
let count = arg_count(c, operands + args.len() - 1)?;
c.emit(
Op::Extended(ext::LSET, count),
1 - (operands + args.len() - 1) as i32,
);
Ok(())
}
fn lpop(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
if args.is_empty() {
return c.error("wrong # args: should be \"lpop listvar ?index?\"");
}
let target = c.target_of(&args[0])?;
let operands = var_target(c, &target)?;
for arg in &args[1..] {
c.word(arg)?;
}
let count = arg_count(c, operands + args.len() - 1)?;
c.emit(
Op::Extended(ext::LPOP, count),
1 - (operands + args.len() - 1) as i32,
);
Ok(())
}
fn ledit(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
if args.len() < 3 {
return c.error("wrong # args: should be \"ledit listVar first last ?element ...?\"");
}
let target = c.target_of(&args[0])?;
let operands = var_target(c, &target)?;
for arg in &args[1..] {
c.word(arg)?;
}
let count = arg_count(c, operands + args.len() - 1)?;
c.emit(
Op::Extended(ext::LEDIT, count),
1 - (operands + args.len() - 1) as i32,
);
Ok(())
}
fn lmap(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
const USAGE: &str = "wrong # args: should be \"lmap varList list ?varList list ...? command\"";
let Some((body, pairs)) = args.split_last() else {
return c.error(USAGE);
};
if pairs.is_empty() || pairs.len() % 2 != 0 {
return c.error(USAGE);
}
let mut names = Vec::new();
for pair in pairs.chunks(2) {
let text = c.literal_of(&pair[0], "lmap variable list")?.to_string();
let vars = list::split(&text).map_err(|msg| c.err(msg))?;
if vars.is_empty() {
return c.error("lmap varlist is empty");
}
let width = vars.len();
names.extend(vars);
c.push_value(Value::Int(width as i64));
c.word(&pair[1])?;
}
let lists = u8::try_from(pairs.len() / 2)
.map_err(|_| c.err("too many lists for \"lmap\"".to_string()))?;
let width = u8::try_from(names.len())
.map_err(|_| c.err("too many variables for \"lmap\"".to_string()))?;
c.emit(Op::Extended(ext::LMAP_INIT, lists), 1 - pairs.len() as i32);
let script = c.body_of(body)?;
let taken: Vec<String> = names.iter().rev().cloned().collect();
c.rotated_loop(
|c| {
c.emit(Op::Extended(ext::FOREACH_TAKE, width), i32::from(width));
for name in &taken {
c.store_named(name)?;
}
c.emit_body_value(&script)?;
c.emit(Op::Extended(ext::LMAP_COLLECT, 0), -1);
Ok(())
},
|c| {
c.emit(Op::Extended(ext::FOREACH_ADVANCE, 0), 0);
Ok(())
},
|c| {
c.emit(Op::Extended(ext::FOREACH_MORE, 0), 1);
Ok(())
},
)?;
c.emit(Op::Extended(ext::LMAP_RESULT, 0), 0);
Ok(())
}
fn arg_count(c: &Compiler, len: usize) -> Result<u8, CompileError> {
u8::try_from(len).map_err(|_| CompileError {
msg: "too many arguments for a list command".to_string(),
line: c.line,
})
}
pub(crate) fn run(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
if (ext::FOREACH_INIT..=ext::FOREACH_ADVANCE).contains(&id) {
return foreach_op(vm, id, arg);
}
if id == ext::LAPPEND_VAR || id == ext::LAPPEND_SLOT {
return lappend_at(vm, id, arg);
}
if (ext::LMAP_INIT..=ext::LMAP_RESULT).contains(&id) {
return lmap_op(vm, id, arg);
}
if id == ext::LASSIGN {
return lassign_op(vm, arg);
}
if matches!(id, ext::LSET | ext::LPOP | ext::LEDIT) {
return list_var_op(vm, id, arg);
}
let mut values: Vec<Value> = (0..arg).map(|_| vm.pop()).collect();
values.reverse();
let result = match id {
ext::LINDEX => lindex_op(&values)?,
ext::LLENGTH if values.len() == 1 => elements(&values[0])?.len().to_string(),
_ => {
let args: Vec<String> = values.iter().map(to_tcl_string).collect();
dispatch(id, &args)?
}
};
vm.push(Value::Str(Arc::new(result)));
Ok(())
}
type SplitEntry = (Arc<String>, Arc<Vec<String>>);
thread_local! {
static SPLIT: RefCell<Vec<SplitEntry>> = const { RefCell::new(Vec::new()) };
}
const SPLIT_ENTRIES: usize = 4;
fn elements(value: &Value) -> Result<Arc<Vec<String>>, String> {
let Value::Str(text) = value else {
return Ok(Arc::new(list::split(&to_tcl_string(value))?));
};
let hit = SPLIT.with(|cache| {
cache
.borrow()
.iter()
.find(|(key, _)| Arc::ptr_eq(key, text))
.map(|(_, items)| Arc::clone(items))
});
if let Some(items) = hit {
return Ok(items);
}
let items = Arc::new(list::split(text)?);
SPLIT.with(|cache| {
let mut cache = cache.borrow_mut();
if cache.len() == SPLIT_ENTRIES {
cache.remove(0);
}
cache.push((Arc::clone(text), Arc::clone(&items)));
});
Ok(items)
}
pub(crate) fn forget_split() {
SPLIT.with(|cache| cache.borrow_mut().clear());
}
fn lindex_op(values: &[Value]) -> Result<String, String> {
let indices: Vec<String> = values[1..].iter().map(to_tcl_string).collect();
if indices.len() == 1 && list::index(&indices[0], i64::MAX - 1).is_ok() {
let items = elements(&values[0])?;
let at = list::index(&indices[0], items.len() as i64 - 1)?;
if at < 0 || at >= items.len() as i64 {
return Ok(String::new());
}
return Ok(items[at as usize].clone());
}
lindex(&to_tcl_string(&values[0]), &indices)
}
pub(crate) fn lsort_op(interp: &Shared, vm: &mut VM, arg: u8) -> Result<(), String> {
let mut args: Vec<String> = (0..arg).map(|_| to_tcl_string(&vm.pop())).collect();
args.reverse();
let result = crate::runtime::at_global(interp, vm, |interp| lsort(&args, Some(interp)))?;
vm.push(Value::Str(Arc::new(result)));
Ok(())
}
fn dispatch(id: u16, args: &[String]) -> Result<String, String> {
match id {
ext::LIST => Ok(list::join(args)),
ext::LLENGTH => Ok(list::length(&args[0])?.to_string()),
ext::LINDEX => lindex(&args[0], &args[1..]),
ext::LAPPEND => lappend_value(&args[0], &args[1..]),
ext::LRANGE => lrange(&args[0], &args[1], &args[2]),
ext::LREVERSE => {
let mut items = list::split(&args[0])?;
items.reverse();
Ok(list::join(&items))
}
ext::LINSERT => linsert(&args[0], &args[1], &args[2..]),
ext::LREPLACE => lreplace(&args[0], &args[1], &args[2], &args[3..]),
ext::LSEARCH => lsearch(args),
ext::LSORT => lsort(args, None),
ext::JOIN => {
let sep = args.get(1).map(String::as_str).unwrap_or(" ");
Ok(list::split(&args[0])?.join(sep))
}
ext::SPLIT => Ok(split(&args[0], args.get(1).map_or(" \n\t\r", |s| s))),
ext::CONCAT => Ok(concat(args)),
ext::LREPEAT => lrepeat(&args[0], &args[1..]),
ext::LREMOVE => lremove(&args[0], &args[1..]),
ext::LSEQ => lseq(args),
other => Err(format!("unknown list op {other}")),
}
}
fn lrepeat(count: &str, values: &[String]) -> Result<String, String> {
let n = list::wide(count).map_err(|_| format!("expected integer but got \"{count}\""))?;
if n < 0 {
return Err(format!("bad count \"{count}\": must be integer >= 0"));
}
let mut out = Vec::with_capacity(values.len() * n.max(0) as usize);
for _ in 0..n {
out.extend(values.iter().cloned());
}
Ok(list::join(&out))
}
fn lremove(value: &str, indices: &[String]) -> Result<String, String> {
let items = list::split(value)?;
let end = items.len() as i64 - 1;
let mut drop = vec![false; items.len()];
for text in indices {
let at = list::index(text, end)?;
if at >= 0 && at < items.len() as i64 {
drop[at as usize] = true;
}
}
let kept: Vec<String> = items
.into_iter()
.enumerate()
.filter(|(i, _)| !drop[*i])
.map(|(_, v)| v)
.collect();
Ok(list::join(&kept))
}
fn lseq(args: &[String]) -> Result<String, String> {
const USAGE: &str = "wrong # args: should be \"lseq n ??op? n ??by? n??\"";
if args.is_empty() {
return Err(USAGE.to_string());
}
let (from, to, step, by_count) = if args.len() == 1 {
(None, &args[0], None, false)
} else {
let anchored = args.len() == 5 && args[3] == "by";
let (op, to_at) = if is_lseq_op(&args[1]) {
(Some(args[1].as_str()), 2)
} else if anchored {
return Err(format!("expected number but got \"{}\"", args[1]));
} else {
(None, 1)
};
let Some(to) = args.get(to_at) else {
return Err(USAGE.to_string());
};
let rest = &args[to_at + 1..];
let step = match rest {
[] => None,
[s] if op.is_none() => Some(s),
[s] if s == "by" => return Err("missing \"by\" value.".to_string()),
[_] => return Err(USAGE.to_string()),
[by, s] if by == "by" => Some(s),
[other, _] if is_lseq_op(other) => return Err(USAGE.to_string()),
[other, _] => {
return Err(format!(
"bad operation \"{other}\": must be .., to, count, or by"
))
}
_ => return Err(USAGE.to_string()),
};
(Some(&args[0]), to, step, op == Some("count"))
};
let number = |t: &str| list::parse_double(t).ok_or(format!("expected number but got \"{t}\""));
let integral = |t: &str| list::parse_int(t).is_some();
let (start, count_form) = match from {
Some(a) => (number(a)?, by_count),
None => (0.0, false),
};
let limit = number(to)?;
let stride = match step {
Some(s) => number(s)?,
None => {
if count_form || from.is_none() {
1.0
} else if limit < start {
-1.0
} else {
1.0
}
}
};
let counting = count_form || from.is_none();
let floating = from.is_some_and(|a| !integral(a))
|| step.is_some_and(|s| !integral(s))
|| (!counting && !integral(to));
let mut out: Vec<String> = Vec::new();
let mut push = |v: f64| {
out.push(if floating {
crate::runtime::format_double(v)
} else {
(v as i64).to_string()
});
};
if count_form {
let n = limit as i64;
for i in 0..n.max(0) {
push(start + stride * i as f64);
}
return Ok(list::join(&out));
}
if from.is_none() {
let n = limit as i64;
for i in 0..n.max(0) {
push(i as f64);
}
return Ok(list::join(&out));
}
if stride == 0.0 {
push(start);
return Ok(list::join(&out));
}
let mut at = start;
let span = (limit - start) / stride;
if span < 0.0 {
return Ok(String::new());
}
let iterations = span.floor() as i64;
for _ in 0..=iterations {
push(at);
at += stride;
}
Ok(list::join(&out))
}
fn is_lseq_op(text: &str) -> bool {
matches!(text, ".." | "to" | "count" | "by")
}
fn lindex(value: &str, indices: &[String]) -> Result<String, String> {
if indices.len() == 1 && list::index(&indices[0], i64::MAX - 1).is_err() {
let path = list::split(&indices[0]).unwrap_or_else(|_| vec![indices[0].clone()]);
return lindex_flat(value, &path);
}
lindex_flat(value, indices)
}
fn lindex_flat(value: &str, indices: &[String]) -> Result<String, String> {
let mut current = value.to_string();
for (i, text) in indices.iter().enumerate() {
let items = list::split(¤t)?;
let at = list::index(text, items.len() as i64 - 1)?;
if at < 0 || at >= items.len() as i64 {
for rest in &indices[i + 1..] {
list::index(rest, i64::MAX - 1)?;
}
return Ok(String::new());
}
current = items[at as usize].clone();
}
Ok(current)
}
fn lappend_value(current: &str, values: &[String]) -> Result<String, String> {
let mut items = list::split(current)?;
if values.is_empty() {
return Ok(current.to_string());
}
items.extend(values.iter().cloned());
Ok(list::join(&items))
}
thread_local! {
static CANONICAL: RefCell<Option<Arc<String>>> = const { RefCell::new(None) };
}
fn lappend_at(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
let mut values: Vec<String> = (1..arg).map(|_| to_tcl_string(&vm.pop())).collect();
values.reverse();
let place = place_of(vm, id == ext::LAPPEND_SLOT)?;
let current = take_var(vm, place);
let extended = extend(current, &values)?;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::clone(&extended));
}
remember(&extended);
vm.push(Value::Str(extended));
Ok(())
}
fn extend(current: Value, values: &[String]) -> Result<Arc<String>, String> {
if let Value::Str(list) = current {
if forget(&list) {
return Ok(append_canonical(list, values));
}
return Ok(Arc::new(lappend_value(&list, values)?));
}
Ok(Arc::new(lappend_value(&to_tcl_string(¤t), values)?))
}
fn append_canonical(mut list: Arc<String>, values: &[String]) -> Arc<String> {
match Arc::get_mut(&mut list) {
Some(text) => {
for value in values {
push_element(text, value);
}
list
}
None => {
let extra: usize = values.iter().map(|value| value.len() + 3).sum();
let mut text = String::with_capacity(list.len() + extra);
text.push_str(&list);
for value in values {
push_element(&mut text, value);
}
Arc::new(text)
}
}
}
fn push_element(out: &mut String, value: &str) {
if out.is_empty() {
out.push_str(&list::quote(value, true));
} else {
out.push(' ');
out.push_str(&list::quote(value, false));
}
}
fn remember(list: &Arc<String>) {
CANONICAL.with(|canonical| *canonical.borrow_mut() = Some(Arc::clone(list)));
}
fn forget(list: &Arc<String>) -> bool {
CANONICAL.with(|canonical| {
let mut remembered = canonical.borrow_mut();
match &*remembered {
Some(previous) if Arc::ptr_eq(previous, list) => {
*remembered = None;
true
}
_ => false,
}
})
}
fn lrange(value: &str, first: &str, last: &str) -> Result<String, String> {
let items = list::split(value)?;
let end = items.len() as i64 - 1;
let first = list::index(first, end)?.max(0);
let last = list::index(last, end)?.min(end);
if first > last {
return Ok(String::new());
}
Ok(list::join(&items[first as usize..=last as usize]))
}
fn linsert(value: &str, index: &str, elements: &[String]) -> Result<String, String> {
let mut items = list::split(value)?;
let at = list::index(index, items.len() as i64)?.clamp(0, items.len() as i64) as usize;
items.splice(at..at, elements.iter().cloned());
Ok(list::join(&items))
}
fn lreplace(value: &str, first: &str, last: &str, elements: &[String]) -> Result<String, String> {
let mut items = list::split(value)?;
let len = items.len() as i64;
let first = list::index(first, len - 1)?.clamp(0, len);
let last = list::index(last, len - 1)?.min(len - 1);
let deleted = if first <= last {
(last - first + 1) as usize
} else {
0
};
let at = first as usize;
items.splice(at..at + deleted, elements.iter().cloned());
Ok(list::join(&items))
}
fn split(value: &str, chars: &str) -> String {
if value.is_empty() {
return String::new();
}
if chars.is_empty() {
let items: Vec<String> = value.chars().map(String::from).collect();
return list::join(&items);
}
let items: Vec<String> = value
.split(|c| chars.contains(c))
.map(str::to_string)
.collect();
list::join(&items)
}
pub(crate) fn concat(args: &[String]) -> String {
let space = |c: char| c.is_ascii() && list::is_space(c as u8);
let mut out = String::new();
let mut emitted = false;
for arg in args {
let start = arg.len() - arg.trim_start_matches(space).len();
let mut end = arg.trim_end_matches(space).len();
if end <= start {
continue;
}
if end < arg.len() && arg[start..end].ends_with('\\') {
end += 1;
}
if emitted {
out.push(' ');
}
out.push_str(&arg[start..end]);
emitted = true;
}
out
}
const LSEARCH_OPTIONS: &[&str] = &[
"-all",
"-ascii",
"-bisect",
"-decreasing",
"-dictionary",
"-exact",
"-glob",
"-increasing",
"-index",
"-inline",
"-integer",
"-nocase",
"-not",
"-real",
"-regexp",
"-sorted",
"-start",
"-stride",
"-subindices",
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Exact,
Glob,
Regexp,
Sorted,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DataType {
Ascii,
AsciiNoCase,
Dictionary,
Integer,
Real,
Command,
}
fn lsearch(args: &[String]) -> Result<String, String> {
let mut mode = Mode::Glob;
let mut data = DataType::Ascii;
let mut all = false;
let mut inline = false;
let mut negated = false;
let mut start_text: Option<&str> = None;
let mut increasing = true;
let mut bisect = false;
let mut nocase = false;
let mut subindices = false;
let mut index_path: Vec<String> = Vec::new();
let mut stride = 1usize;
let mut i = 0;
while i + 2 < args.len() {
let name = LSEARCH_OPTIONS[option(LSEARCH_OPTIONS, &args[i])?];
match name {
"-all" => all = true,
"-ascii" => data = DataType::Ascii,
"-dictionary" => data = DataType::Dictionary,
"-nocase" => nocase = true,
"-subindices" => subindices = true,
"-index" => {
if i + 2 > args.len() - 2 {
return Err("\"-index\" option must be followed by list index".to_string());
}
i += 1;
index_path = list::split(&args[i])?;
}
"-exact" => mode = Mode::Exact,
"-glob" => mode = Mode::Glob,
"-regexp" => mode = Mode::Regexp,
"-inline" => inline = true,
"-integer" => data = DataType::Integer,
"-not" => negated = true,
"-real" => data = DataType::Real,
"-increasing" => increasing = true,
"-decreasing" => increasing = false,
"-start" => {
if i + 2 > args.len() - 2 {
return Err("missing starting index".to_string());
}
i += 1;
start_text = Some(&args[i]);
}
"-sorted" | "-bisect" => {
mode = Mode::Sorted;
bisect = name == "-bisect";
}
"-stride" => {
if i + 2 > args.len() - 2 {
return Err(
"\"-stride\" option must be followed by a stride length".to_string()
);
}
i += 1;
let n = list::wide(&args[i])?;
if n < 1 {
return Err("stride length must be at least 1".to_string());
}
stride = n as usize;
}
other => return Err(format!("lsearch {other} is not supported yet")),
}
i += 1;
}
if subindices && index_path.is_empty() {
return Err("-subindices cannot be used without -index option".to_string());
}
if bisect && mode == Mode::Sorted && (all || negated) {
return Err("-bisect is not compatible with -all or -not".to_string());
}
let items = list::split(&args[args.len() - 2])?;
let pattern = &args[args.len() - 1];
if stride > 1 && !items.len().is_multiple_of(stride) {
return Err("list size must be a multiple of the stride length".to_string());
}
let (group_offset, path): (usize, &[String]) = match index_path.split_first() {
Some((first, rest)) if stride > 1 => {
let at = list::index(first, stride as i64 - 1)?;
if at < 0 || at >= stride as i64 {
return Err("when used with \"-stride\", the leading \"-index\" value must be within the group".to_string());
}
(at as usize, rest)
}
_ => (0, &index_path),
};
let mut start = 0usize;
if let Some(text) = start_text {
let at = list::index(text, items.len() as i64 - 1)?.max(0);
if at >= items.len() as i64 {
return Ok(if all || inline {
String::new()
} else {
"-1".to_string()
});
}
start = at as usize;
start -= start % stride;
}
let key_at = |i: usize| -> Result<String, String> { sublist(&items[i + group_offset], path) };
let mut index: i64 = -1;
let mut hits: Vec<usize> = Vec::new();
if mode == Mode::Sorted && !all && !negated {
let mut lower = start as i64 - stride as i64;
let mut upper = items.len() as i64;
while lower + (stride as i64) != upper {
let mut i = (lower + upper) / 2;
i -= i % stride as i64;
let key = key_at(i as usize)?;
let order = compare_to_pattern(pattern, &key, data, nocase)?;
if order.is_eq() {
index = i;
if bisect {
lower = i;
} else {
upper = i;
}
} else if order.is_gt() == increasing {
lower = i;
} else {
upper = i;
}
}
if bisect && index < 0 {
index = lower;
}
if index >= 0 {
hits.push(index as usize);
}
} else {
let target = match (mode, data) {
(Mode::Exact | Mode::Sorted, DataType::Integer) => {
Some(Compare::Integer(list::wide(pattern)?))
}
(Mode::Exact | Mode::Sorted, DataType::Real) => {
Some(Compare::Real(list::double(pattern)?))
}
_ => None,
};
let mut i = start;
while i < items.len() {
let item = key_at(i)?;
let mut hit = match (&target, mode) {
(Some(Compare::Integer(want)), _) => list::wide(&item)? == *want,
(Some(Compare::Real(want)), _) => list::double(&item)? == *want,
(None, Mode::Exact | Mode::Sorted) => match data {
DataType::Dictionary => dictionary_compare(&item, pattern).is_eq(),
_ if nocase => {
item.len() == pattern.len() && utf_casecmp(&item, pattern).is_eq()
}
_ => item == *pattern,
},
(None, Mode::Glob) => {
if nocase {
list::glob_match(&pattern.to_lowercase(), &item.to_lowercase())
} else {
list::glob_match(pattern, &item)
}
}
(None, Mode::Regexp) => crate::regexp::matches_anywhere(pattern, &item, nocase)?,
};
if negated {
hit = !hit;
}
if hit {
hits.push(i);
if !all {
break;
}
}
i += stride;
}
}
let group_of = |i: usize| -> Vec<String> { items[i..i + stride].to_vec() };
let subindex_of = |i: usize| -> Result<String, String> {
let mut out = vec![(i + group_offset).to_string()];
for spec in path {
out.push(list::index(spec, items.len() as i64)?.to_string());
}
Ok(list::join(&out))
};
Ok(match (all, inline, subindices) {
(true, true, true) => {
let mut values = Vec::with_capacity(hits.len());
for &i in &hits {
values.push(key_at(i)?);
}
list::join(&values)
}
(true, true, false) => {
let values: Vec<String> = hits.iter().flat_map(|&i| group_of(i)).collect();
list::join(&values)
}
(true, false, true) => {
let mut values = Vec::with_capacity(hits.len());
for &i in &hits {
values.push(subindex_of(i)?);
}
list::join(&values)
}
(true, false, false) => {
let values: Vec<String> = hits.iter().map(|i| i.to_string()).collect();
list::join(&values)
}
(false, true, true) => match hits.first() {
Some(&i) => key_at(i)?,
None => String::new(),
},
(false, true, false) => hits.first().map_or(String::new(), |&i| {
if stride > 1 {
list::join(&group_of(i))
} else {
items[i].clone()
}
}),
(false, false, true) => match hits.first() {
Some(&i) => subindex_of(i)?,
None => "-1".to_string(),
},
(false, false, false) => hits.first().map_or(-1, |&i| i as i64).to_string(),
})
}
fn compare_to_pattern(
pattern: &str,
item: &str,
data: DataType,
nocase: bool,
) -> Result<std::cmp::Ordering, String> {
Ok(match data {
DataType::Dictionary => dictionary_compare(pattern, item),
DataType::Integer => list::wide(pattern)?.cmp(&list::wide(item)?),
DataType::Real => {
let (p, o) = (list::double(pattern)?, list::double(item)?);
match (p >= o, p <= o) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => std::cmp::Ordering::Equal,
}
}
_ if nocase => utf_casecmp(pattern, item),
_ => pattern.cmp(item),
})
}
enum Compare {
Integer(i64),
Real(f64),
}
const LSORT_OPTIONS: &[&str] = &[
"-ascii",
"-command",
"-decreasing",
"-dictionary",
"-increasing",
"-index",
"-indices",
"-integer",
"-nocase",
"-real",
"-stride",
"-unique",
];
enum Key {
Text(String),
TextNoCase(String),
Dictionary(String),
Integer(i64),
Real(f64),
Command(String),
}
pub(crate) enum Comparator<'a> {
Builtin,
Command {
interp: &'a Shared,
prefix: Vec<String>,
failed: RefCell<Option<String>>,
},
}
impl Comparator<'_> {
fn keys(&self, a: &Key, b: &Key) -> std::cmp::Ordering {
let Comparator::Command {
interp,
prefix,
failed,
} = self
else {
return compare_keys(a, b);
};
if failed.borrow().is_some() {
return std::cmp::Ordering::Equal;
}
let (Key::Command(x), Key::Command(y)) = (a, b) else {
return std::cmp::Ordering::Equal;
};
let mut words = prefix.clone();
words.push(x.clone());
words.push(y.clone());
match call(interp, &words) {
Ok(order) => order.cmp(&0),
Err(msg) => {
*failed.borrow_mut() = Some(msg);
std::cmp::Ordering::Equal
}
}
}
fn failure(&self) -> Option<String> {
match self {
Comparator::Builtin => None,
Comparator::Command { failed, .. } => failed.borrow().clone(),
}
}
}
fn call(interp: &Shared, words: &[String]) -> Result<i32, String> {
let value = crate::runtime::run_source(interp, &list::join(words)).map_err(|e| e.msg)?;
let text = crate::runtime::to_tcl_string(&value);
list::wide(&text)
.ok()
.and_then(|n| i32::try_from(n).ok())
.ok_or_else(|| "-compare command returned non-integer result".to_string())
}
fn key_of(text: &str, data: DataType) -> Result<Key, String> {
Ok(match data {
DataType::Ascii => Key::Text(text.to_string()),
DataType::AsciiNoCase => Key::TextNoCase(text.to_string()),
DataType::Dictionary => Key::Dictionary(text.to_string()),
DataType::Integer => Key::Integer(list::wide(text)?),
DataType::Real => Key::Real(list::double(text)?),
DataType::Command => Key::Command(text.to_string()),
})
}
fn utf_casecmp(left: &str, right: &str) -> std::cmp::Ordering {
let mut l = left.chars();
let mut r = right.chars();
loop {
match (l.next(), r.next()) {
(Some(a), Some(b)) => {
if a != b {
let (a, b) = (lower(a), lower(b));
if a != b {
return (a as u32).cmp(&(b as u32));
}
}
}
(rest_left, rest_right) => {
return rest_left.is_some().cmp(&rest_right.is_some());
}
}
}
}
fn lower(c: char) -> char {
let mut it = c.to_lowercase();
match (it.next(), it.next()) {
(Some(one), None) => one,
_ => c,
}
}
fn dictionary_compare(left: &str, right: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
let l: Vec<char> = left.chars().collect();
let r: Vec<char> = right.chars().collect();
let (mut i, mut j) = (0usize, 0usize);
let mut secondary = Ordering::Equal;
let digit = |v: &Vec<char>, at: usize| v.get(at).is_some_and(char::is_ascii_digit);
loop {
if digit(&l, i) && digit(&r, j) {
let mut zeros = 0i32;
while r.get(j) == Some(&'0') && digit(&r, j + 1) {
j += 1;
zeros -= 1;
}
while l.get(i) == Some(&'0') && digit(&l, i + 1) {
i += 1;
zeros += 1;
}
if secondary == Ordering::Equal {
secondary = zeros.cmp(&0);
}
let mut diff = Ordering::Equal;
loop {
if diff == Ordering::Equal {
diff = l.get(i).cmp(&r.get(j));
}
i += 1;
j += 1;
if !digit(&r, j) {
if digit(&l, i) {
return Ordering::Greater;
}
if diff != Ordering::Equal {
return diff;
}
break;
} else if !digit(&l, i) {
return Ordering::Less;
}
}
continue;
}
let (Some(&a), Some(&b)) = (l.get(i), r.get(j)) else {
let ended = l.get(i).is_some().cmp(&r.get(j).is_some());
return if ended == Ordering::Equal {
secondary
} else {
ended
};
};
i += 1;
j += 1;
let (al, bl) = (lower(a), lower(b));
if al != bl {
return (al as u32).cmp(&(bl as u32));
}
if secondary == Ordering::Equal {
if a.is_uppercase() && b.is_lowercase() {
secondary = Ordering::Less;
} else if b.is_uppercase() && a.is_lowercase() {
secondary = Ordering::Greater;
}
}
}
}
fn sublist(item: &str, indices: &[String]) -> Result<String, String> {
let mut current = item.to_string();
for spec in indices {
let elements = list::split(¤t)?;
let at = list::index(spec, elements.len() as i64 - 1)?;
let Some(found) = usize::try_from(at).ok().and_then(|k| elements.get(k)) else {
return Err(format!(
"element {} missing from sublist \"{current}\"",
missing_index(spec, at)
));
};
current = found.clone();
}
Ok(current)
}
fn missing_index(spec: &str, at: i64) -> String {
if at < 0 && spec.starts_with("end") {
return spec.to_string();
}
at.to_string()
}
struct Element {
key: Key,
payload: usize,
next: Option<usize>,
}
fn lsort_stride(
items: &[String],
stride: usize,
data: DataType,
order: Order,
indices: bool,
index_path: &[String],
cmp: &Comparator,
) -> Result<String, String> {
let (offset, rest) = match index_path.split_first() {
None => (0usize, &[][..]),
Some((first, rest)) => {
let at = list::index(first, stride as i64 - 1)?;
if at < 0 || at >= stride as i64 {
return Err("when used with \"-stride\", the leading \"-index\" value must be within the group".to_string());
}
(at as usize, rest)
}
};
let groups = items.len() / stride;
let mut keyed: Vec<(Key, usize)> = Vec::with_capacity(groups);
for g in 0..groups {
let keyed_on = &items[g * stride + offset];
keyed.push((key_of(&sublist(keyed_on, rest)?, data)?, g));
}
keyed.sort_by(|a, b| {
let ord = cmp.keys(&a.0, &b.0);
if order.increasing {
ord
} else {
ord.reverse()
}
});
let mut out = Vec::with_capacity(items.len());
for (i, (key, g)) in keyed.iter().enumerate() {
if order.unique {
if let Some((next, _)) = keyed.get(i + 1) {
if cmp.keys(key, next).is_eq() {
continue;
}
}
}
if indices {
out.extend((g * stride..(g + 1) * stride).map(|k| k.to_string()));
} else {
out.extend_from_slice(&items[g * stride..(g + 1) * stride]);
}
}
Ok(list::join(&out))
}
fn lsort(args: &[String], interp: Option<&Shared>) -> Result<String, String> {
let mut data = DataType::Ascii;
let mut command: Vec<String> = Vec::new();
let mut increasing = true;
let mut unique = false;
let mut indices = false;
let mut nocase = false;
let mut index_path: Vec<String> = Vec::new();
let mut stride = 1usize;
let mut i = 0;
while i + 1 < args.len() {
let name = LSORT_OPTIONS[option(LSORT_OPTIONS, &args[i])?];
match name {
"-ascii" => data = DataType::Ascii,
"-decreasing" => increasing = false,
"-dictionary" => data = DataType::Dictionary,
"-increasing" => increasing = true,
"-indices" => indices = true,
"-integer" => data = DataType::Integer,
"-nocase" => nocase = true,
"-real" => data = DataType::Real,
"-unique" => unique = true,
"-command" => {
let Some(value) = args.get(i + 1) else {
return Err(
"\"-command\" option must be followed by comparison command".to_string()
);
};
command = list::split(value)?;
data = DataType::Command;
i += 1;
}
"-index" => {
let Some(value) = args.get(i + 1) else {
return Err("\"-index\" option must be followed by list index".to_string());
};
index_path = list::split(value)?;
i += 1;
}
"-stride" => {
let Some(value) = args.get(i + 1) else {
return Err(
"\"-stride\" option must be followed by a stride length".to_string()
);
};
let n = list::wide(value)?;
if n < 2 {
return Err("stride length must be at least 2".to_string());
}
stride = n as usize;
i += 1;
}
other => return Err(format!("lsort {other} is not supported yet")),
}
i += 1;
}
if nocase && data == DataType::Ascii {
data = DataType::AsciiNoCase;
}
let cmp = match (data, interp) {
(DataType::Command, Some(interp)) => Comparator::Command {
interp,
prefix: command,
failed: RefCell::new(None),
},
_ => Comparator::Builtin,
};
let items = list::split(&args[args.len() - 1])?;
if stride > 1 {
if !items.len().is_multiple_of(stride) {
return Err("list size must be a multiple of the stride length".to_string());
}
let out = lsort_stride(
&items,
stride,
data,
Order { increasing, unique },
indices,
&index_path,
&cmp,
);
return match cmp.failure() {
Some(msg) => Err(msg),
None => out,
};
}
let mut elements = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
elements.push(Element {
key: key_of(&sublist(item, &index_path)?, data)?,
payload: i,
next: None,
});
}
if elements.is_empty() {
return Ok(String::new());
}
let order = Order { increasing, unique };
const RUNS: usize = 30;
let mut sublists: [Option<usize>; RUNS] = [None; RUNS];
for i in 0..elements.len() {
let mut head = Some(i);
let mut j = 0;
while j < RUNS && sublists[j].is_some() {
let left = sublists[j].take();
head = merge(&mut elements, left, head, order, &cmp);
j += 1;
}
sublists[j.min(RUNS - 1)] = head;
}
let mut head = sublists[0];
for &run in &sublists[1..] {
head = merge(&mut elements, run, head, order, &cmp);
}
let mut sorted = Vec::new();
let mut cursor = head;
while let Some(i) = cursor {
sorted.push(if indices {
elements[i].payload.to_string()
} else {
items[elements[i].payload].clone()
});
cursor = elements[i].next;
}
match cmp.failure() {
Some(msg) => Err(msg),
None => Ok(list::join(&sorted)),
}
}
#[derive(Clone, Copy)]
struct Order {
increasing: bool,
unique: bool,
}
fn compare_keys(a: &Key, b: &Key) -> std::cmp::Ordering {
match (a, b) {
(Key::Text(x), Key::Text(y)) => x.cmp(y),
(Key::TextNoCase(x), Key::TextNoCase(y)) => utf_casecmp(x, y),
(Key::Dictionary(x), Key::Dictionary(y)) => dictionary_compare(x, y),
(Key::Integer(x), Key::Integer(y)) => x.cmp(y),
(Key::Real(x), Key::Real(y)) => {
match (x >= y, x <= y) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => std::cmp::Ordering::Equal,
}
}
_ => std::cmp::Ordering::Equal,
}
}
fn compare(
elements: &[Element],
a: usize,
b: usize,
order: Order,
cmp: &Comparator,
) -> std::cmp::Ordering {
let ordering = cmp.keys(&elements[a].key, &elements[b].key);
if order.increasing {
ordering
} else {
ordering.reverse()
}
}
fn merge(
elements: &mut [Element],
left: Option<usize>,
right: Option<usize>,
order: Order,
cmp: &Comparator,
) -> Option<usize> {
let (Some(first_left), Some(first_right)) = (left, right) else {
return left.or(right);
};
let (mut left, mut right) = (left, right);
let ordering = compare(elements, first_left, first_right, order, cmp);
let head = if ordering.is_gt() || (ordering.is_eq() && order.unique) {
if ordering.is_eq() {
left = elements[first_left].next;
}
right = elements[first_right].next;
first_right
} else {
left = elements[first_left].next;
first_left
};
let mut tail = head;
while let (Some(l), Some(r)) = (left, right) {
let ordering = compare(elements, l, r, order, cmp);
let take_right = if order.unique {
ordering.is_ge()
} else {
ordering.is_gt()
};
if take_right {
if order.unique && ordering.is_eq() {
left = elements[l].next;
}
elements[tail].next = Some(r);
tail = r;
right = elements[r].next;
} else {
elements[tail].next = Some(l);
tail = l;
left = elements[l].next;
}
}
elements[tail].next = left.or(right);
Some(head)
}
fn option(table: &[&str], word: &str) -> Result<usize, String> {
if let Some(i) = table.iter().position(|&name| name == word) {
return Ok(i);
}
let mut hits = table
.iter()
.enumerate()
.filter(|(_, name)| !word.is_empty() && name.starts_with(word));
match (hits.next(), hits.next()) {
(Some((i, _)), None) => Ok(i),
(Some(_), Some(_)) => Err(format!(
"ambiguous option \"{word}\": must be {}",
names(table)
)),
_ => Err(format!("bad option \"{word}\": must be {}", names(table))),
}
}
fn names(table: &[&str]) -> String {
match table {
[] => String::new(),
[only] => only.to_string(),
[first @ .., last] => format!("{}, or {last}", first.join(", ")),
}
}
fn lassign_op(vm: &mut VM, arg: u8) -> Result<(), String> {
let items = list::split(&to_tcl_string(&vm.pop()))?;
let wanted = arg as usize;
let remainder = if items.len() > wanted {
list::join(&items[wanted..])
} else {
String::new()
};
vm.push(Value::Str(Arc::new(remainder)));
for i in (0..wanted).rev() {
let value = items.get(i).cloned().unwrap_or_default();
vm.push(Value::Str(Arc::new(value)));
}
Ok(())
}
fn list_var_op(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
let count = arg as usize - 4;
let mut rest: Vec<String> = (0..count).map(|_| to_tcl_string(&vm.pop())).collect();
rest.reverse();
let is_elem = matches!(vm.pop(), Value::Int(1));
let index = to_tcl_string(&vm.pop());
let operand = vm.pop();
let slot_form = matches!(vm.pop(), Value::Int(1));
let place = place_at(&operand, slot_form)?;
let name = to_tcl_string(&vm.pop());
let current = if is_elem {
take_element(vm, place, &index)
} else {
take_var(vm, place)
};
if current == Value::Undef {
return Err(format!("can't read \"{name}\": no such variable"));
}
let text = to_tcl_string(¤t);
let (stored, yielded) = match id {
ext::LSET => {
let Some((value, indices)) = rest.split_last() else {
return Err(
"wrong # args: should be \"lset listVar ?index? ?index ...? value\""
.to_string(),
);
};
let new = lset_value(&text, indices, value)?;
(new.clone(), new)
}
ext::LPOP => {
let (new, popped) = lpop_value(&text, &rest)?;
(new, popped)
}
_ => {
let new = ledit_value(&text, &rest[0], &rest[1], &rest[2..])?;
(new.clone(), new)
}
};
let stored = Value::Str(Arc::new(stored));
if is_elem {
if let Some(map) = crate::assoc::elements_of(vm, place) {
map.insert(index, stored);
}
} else if let Some(cell) = var_cell(vm, place) {
*cell = stored;
}
vm.push(Value::Str(Arc::new(yielded)));
Ok(())
}
fn take_element(vm: &mut VM, place: Place, index: &str) -> Value {
match crate::assoc::elements_of(vm, place) {
Some(map) => map.remove(index).unwrap_or(Value::Undef),
None => Value::Undef,
}
}
fn lset_value(value: &str, indices: &[String], replacement: &str) -> Result<String, String> {
let path: Vec<String> = match indices {
[] => Vec::new(),
[single] => {
if list::index(single, i64::MAX - 1).is_err() {
list::split(single)?
} else {
vec![single.clone()]
}
}
many => many.to_vec(),
};
if path.is_empty() {
return Ok(replacement.to_string());
}
lset_path(value, &path, replacement)
}
fn lset_path(value: &str, path: &[String], replacement: &str) -> Result<String, String> {
let Some((first, rest)) = path.split_first() else {
return Ok(replacement.to_string());
};
let mut items = list::split(value)?;
let end = items.len() as i64 - 1;
let at = list::index(first, end)?;
if at < 0 || at > items.len() as i64 {
return Err(format!("index \"{first}\" out of range"));
}
if at == items.len() as i64 {
if !rest.is_empty() {
return Err(format!("index \"{first}\" out of range"));
}
items.push(replacement.to_string());
return Ok(list::join(&items));
}
let at = at as usize;
items[at] = if rest.is_empty() {
replacement.to_string()
} else {
lset_path(&items[at], rest, replacement)?
};
Ok(list::join(&items))
}
fn lpop_value(value: &str, indices: &[String]) -> Result<(String, String), String> {
let path: Vec<String> = if indices.is_empty() {
vec!["end".to_string()]
} else {
indices.to_vec()
};
let popped = lindex_flat(value, &path)?;
let items = list::split(value)?;
let end = items.len() as i64 - 1;
let at = list::index(&path[0], end)?;
if at < 0 || at >= items.len() as i64 {
return Err(format!("index \"{}\" out of range", path[0]));
}
let at = at as usize;
let mut items = items;
if path.len() == 1 {
items.remove(at);
} else {
let inner = lremove_at(&items[at], &path[1..])?;
items[at] = inner;
}
Ok((list::join(&items), popped))
}
fn lremove_at(value: &str, path: &[String]) -> Result<String, String> {
let mut items = list::split(value)?;
let end = items.len() as i64 - 1;
let at = list::index(&path[0], end)?;
if at < 0 || at >= items.len() as i64 {
return Err(format!("index \"{}\" out of range", path[0]));
}
let at = at as usize;
if path.len() == 1 {
items.remove(at);
} else {
items[at] = lremove_at(&items[at], &path[1..])?;
}
Ok(list::join(&items))
}
fn ledit_value(
value: &str,
first: &str,
last: &str,
elements: &[String],
) -> Result<String, String> {
lreplace(value, first, last, elements)
}
fn foreach_op(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::FOREACH_INIT => {
let mut pairs: Vec<(usize, String)> = (0..arg)
.map(|_| {
let text = to_tcl_string(&vm.pop());
let vars = to_tcl_string(&vm.pop()).parse::<usize>().unwrap_or(0);
(vars, text)
})
.collect();
pairs.reverse();
let mut lists = Vec::with_capacity(pairs.len());
let mut iterations = 0usize;
for (vars, text) in &pairs {
let items = list::split(text)?;
iterations = iterations.max(items.len().div_ceil(*vars));
lists.push(items);
}
let mut flat = Vec::new();
for iteration in 0..iterations {
for (list_index, (vars, _)) in pairs.iter().enumerate() {
for slot in 0..*vars {
let at = iteration * vars + slot;
let value = lists[list_index].get(at).cloned().unwrap_or_default();
flat.push(Value::Str(Arc::new(value)));
}
}
}
vm.push(Value::array(vec![
Value::Int(0),
Value::Int(iterations as i64),
Value::array(flat),
]));
Ok(())
}
ext::FOREACH_MORE => {
let (at, total, _) = borrow_state(vm.peek())?;
vm.push(Value::Bool(at < total));
Ok(())
}
ext::FOREACH_TAKE => {
let width = arg as usize;
let (at, _, values) = borrow_state(vm.peek())?;
let row: Vec<Value> = values[at as usize * width..][..width].to_vec();
for value in row {
vm.push(value);
}
Ok(())
}
_ => {
let Value::Array(mut parts) = vm.pop() else {
return Err(CORRUPT.to_string());
};
let Some(Value::Int(at)) = Arc::make_mut(&mut parts).first_mut() else {
return Err(CORRUPT.to_string());
};
*at += 1;
vm.push(Value::Array(parts));
Ok(())
}
}
}
fn lmap_op(vm: &mut VM, id: u16, arg: u8) -> Result<(), String> {
match id {
ext::LMAP_INIT => {
foreach_op(vm, ext::FOREACH_INIT, arg)?;
let Value::Array(mut parts) = vm.pop() else {
return Err(CORRUPT.to_string());
};
Arc::make_mut(&mut parts).push(Value::array(Vec::new()));
vm.push(Value::Array(parts));
Ok(())
}
ext::LMAP_COLLECT => {
let value = to_tcl_string(&vm.pop());
let Value::Array(mut parts) = vm.pop() else {
return Err(CORRUPT.to_string());
};
let Some(Value::Array(acc)) = Arc::make_mut(&mut parts).last_mut() else {
return Err(CORRUPT.to_string());
};
Arc::make_mut(acc).push(Value::Str(Arc::new(value)));
vm.push(Value::Array(parts));
Ok(())
}
_ => {
let Value::Array(parts) = vm.pop() else {
return Err(CORRUPT.to_string());
};
let Some(Value::Array(acc)) = parts.last() else {
return Err(CORRUPT.to_string());
};
let items: Vec<String> = acc.iter().map(to_tcl_string).collect();
vm.push(Value::Str(Arc::new(list::join(&items))));
Ok(())
}
}
}
const CORRUPT: &str = "corrupt foreach state";
fn borrow_state(value: &Value) -> Result<(i64, i64, &[Value]), String> {
match value {
Value::Array(parts) => match parts.as_slice() {
[Value::Int(at), Value::Int(total), Value::Array(values), ..] => {
Ok((*at, *total, values))
}
_ => Err(CORRUPT.to_string()),
},
_ => Err(CORRUPT.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::COMMANDS;
#[test]
fn every_listed_command_runs_as_a_command() {
for name in COMMANDS {
let err = crate::Interp::capturing()
.eval(name)
.err()
.map(|e| e.msg)
.unwrap_or_default();
assert!(
!err.contains("invalid command name"),
"{name} is listed but the compiler does not know it: {err}"
);
}
}
#[test]
fn an_unlisted_name_is_refused_when_it_runs() {
let err = crate::Interp::capturing()
.eval("lnotacommand")
.err()
.map(|e| e.msg)
.unwrap_or_default();
assert!(err.contains("invalid command name"), "got {err:?}");
}
#[test]
fn an_unlisted_name_in_a_branch_never_taken_is_not_an_error() {
let outcome = crate::Interp::capturing()
.eval("if {0} {lnotacommand}\nset x done")
.expect("a branch never taken cannot fail");
assert_eq!(outcome, "done");
}
}