use std::cell::RefCell;
use std::sync::Arc;
use fusevm::{Op, Value, VM};
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};
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 ...?\"");
};
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 (id, place) = match c.var_place(&name) {
Place::Slot(slot) => (ext::LAPPEND_SLOT, slot),
Place::Global(idx) => (ext::LAPPEND_VAR, idx),
};
c.emit(Op::LoadInt(place as i64), 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 names: Vec<String> = vars
.iter()
.map(|w| c.var_name_of(w))
.collect::<Result<_, _>>()?;
c.word(list)?;
let count = arg_count(c, names.len())?;
c.emit(Op::Extended(ext::LASSIGN, count), names.len() as i32);
for name in &names {
c.emit_set_var(name);
}
Ok(())
}
fn var_target(c: &mut Compiler, name: &str) -> usize {
c.push_str(name);
match c.var_place(name) {
Place::Slot(slot) => {
c.push_value(Value::Int(1));
c.emit(Op::LoadInt(slot as i64), 1);
}
Place::Global(idx) => {
c.push_value(Value::Int(0));
c.emit(Op::LoadInt(idx as i64), 1);
}
}
3
}
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 name = c.var_name_of(&args[0])?;
let operands = var_target(c, &name);
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 name = c.var_name_of(&args[0])?;
let operands = var_target(c, &name);
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 name = c.var_name_of(&args[0])?;
let operands = var_target(c, &name);
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();
for name in vars {
if name.ends_with(')') && name.contains('(') {
return c.error("array variables are not supported yet");
}
names.push(name);
}
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.emit_set_var(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 args: Vec<String> = (0..arg).map(|_| to_tcl_string(&vm.pop())).collect();
args.reverse();
let result = dispatch(id, &args)?;
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),
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,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DataType {
Ascii,
Integer,
Real,
}
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 ordered: Option<&str> = None;
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,
"-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" => ordered = Some(name),
"-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 let Some(name) = ordered {
let order = if increasing {
"-increasing"
} else {
"-decreasing"
};
return Err(format!("lsearch {name} {order} is not supported yet"));
}
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 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;
}
let target = match (mode, data) {
(Mode::Exact, DataType::Integer) => Some(Compare::Integer(list::wide(pattern)?)),
(Mode::Exact, DataType::Real) => Some(Compare::Real(list::double(pattern)?)),
_ => None,
};
let mut hits: Vec<usize> = Vec::new();
for (i, item) in items
.iter()
.enumerate()
.skip(start)
.filter(|(i, _)| i.is_multiple_of(stride))
{
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) => item == pattern,
(None, Mode::Glob) => list::glob_match(pattern, item),
(None, Mode::Regexp) => crate::regexp::matches_anywhere(pattern, item, false)?,
};
if negated {
hit = !hit;
}
if hit {
hits.push(i);
if !all {
break;
}
}
}
let group_of = |i: usize| -> Vec<String> { items[i..i + stride].to_vec() };
Ok(match (all, inline) {
(true, true) => {
let values: Vec<String> = hits.iter().flat_map(|&i| group_of(i)).collect();
list::join(&values)
}
(true, false) => {
let values: Vec<String> = hits.iter().map(|i| i.to_string()).collect();
list::join(&values)
}
(false, true) => hits
.first()
.map_or(String::new(), |&i| list::join(&group_of(i))),
(false, false) => hits.first().map_or(-1, |&i| i as i64).to_string(),
})
}
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),
Integer(i64),
Real(f64),
}
struct Element {
key: Key,
payload: usize,
next: Option<usize>,
}
fn lsort_stride(
items: &[String],
stride: usize,
data: DataType,
order: Order,
indices: bool,
) -> Result<String, String> {
let groups = items.len() / stride;
let mut keyed: Vec<(Key, usize)> = Vec::with_capacity(groups);
for g in 0..groups {
let first = &items[g * stride];
keyed.push((
match data {
DataType::Ascii => Key::Text(first.clone()),
DataType::Integer => Key::Integer(list::wide(first)?),
DataType::Real => Key::Real(list::double(first)?),
},
g,
));
}
keyed.sort_by(|a, b| {
let ord = compare_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 compare_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]) -> Result<String, String> {
let mut data = DataType::Ascii;
let mut increasing = true;
let mut unique = false;
let mut indices = false;
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,
"-increasing" => increasing = true,
"-indices" => indices = true,
"-integer" => data = DataType::Integer,
"-real" => data = DataType::Real,
"-unique" => unique = true,
"-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;
}
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());
}
return lsort_stride(&items, stride, data, Order { increasing, unique }, indices);
}
let mut elements = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
elements.push(Element {
key: match data {
DataType::Ascii => Key::Text(item.clone()),
DataType::Integer => Key::Integer(list::wide(item)?),
DataType::Real => Key::Real(list::double(item)?),
},
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);
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);
}
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;
}
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::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) -> std::cmp::Ordering {
let ordering = match (&elements[a].key, &elements[b].key) {
(Key::Text(x), Key::Text(y)) => x.cmp(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,
};
if order.increasing {
ordering
} else {
ordering.reverse()
}
}
fn merge(
elements: &mut [Element],
left: Option<usize>,
right: Option<usize>,
order: Order,
) -> 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);
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);
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 - 3;
let mut rest: Vec<String> = (0..count).map(|_| to_tcl_string(&vm.pop())).collect();
rest.reverse();
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 = 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)
}
};
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::new(stored));
}
vm.push(Value::Str(Arc::new(yielded)));
Ok(())
}
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)) = 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());
};
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)) = parts.last_mut() else {
return Err(CORRUPT.to_string());
};
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.into_iter().next_back() 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");
}
}