use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use fusevm::{Op, Value, VM};
use regex::Regex;
use crate::compiler::{ext as base_ext, CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{place_at, to_tcl_string, var_cell, Shared};
pub mod ext {
pub use crate::compiler::ext::REGEXP_BASE as BASE;
pub const REGEXP: u16 = BASE;
pub const REGSUB: u16 = BASE + 1;
pub const SWITCH_VARS: u16 = BASE + 2;
pub const SWITCH_CLEAR: u16 = BASE + 3;
}
const F_NOCASE: i64 = 1;
const F_ALL: i64 = 1 << 1;
const F_INLINE: i64 = 1 << 2;
const F_INDICES: i64 = 1 << 3;
const F_LINEANCHOR: i64 = 1 << 4;
const F_LINESTOP: i64 = 1 << 5;
const F_EXPANDED: i64 = 1 << 6;
const F_INTO_VAR: i64 = 1 << 7;
const F_COMMAND: i64 = 1 << 8;
pub const COMMANDS: &[&str] = &["regexp", "regsub"];
pub(crate) fn compile(c: &mut Compiler, name: &str, args: &[Word]) -> Result<(), CompileError> {
let regsub = name == "regsub";
let usage = if regsub {
"regsub ?-option ...? exp string subSpec ?varName?"
} else {
"regexp ?-option ...? exp string ?matchVar? ?subMatchVar ...?"
};
let mut flags: i64 = 0;
let mut start: Option<&Word> = None;
let mut i = 0;
while i < args.len() {
let Some(text) = args[i].as_literal() else {
break;
};
if !text.starts_with('-') || text == "-" {
break;
}
i += 1;
match text {
"--" => break,
"-nocase" => flags |= F_NOCASE,
"-all" => flags |= F_ALL,
"-expanded" => flags |= F_EXPANDED,
"-line" => flags |= F_LINEANCHOR | F_LINESTOP,
"-lineanchor" => flags |= F_LINEANCHOR,
"-linestop" => flags |= F_LINESTOP,
"-inline" if !regsub => flags |= F_INLINE,
"-indices" if !regsub => flags |= F_INDICES,
"-command" if regsub => flags |= F_COMMAND,
"-start" => {
let Some(value) = args.get(i) else {
return c.error(format!("wrong # args: should be \"{usage}\""));
};
i += 1;
start = Some(value);
}
"-about" if !regsub => {
c.push_str(
"regexp -about is not supported yet: its second element is the reference \
engine's own compile-time telemetry, which this engine can only infer",
);
c.emit(Op::Extended(base_ext::ERROR, 0), -1);
c.push_empty();
return Ok(());
}
other => {
c.push_str(&format!(
"bad option \"{other}\": must be {}",
if regsub {
"-all, -command, -expanded, -line, -linestop, -lineanchor, -nocase, -start, or --"
} else {
"-all, -about, -indices, -inline, -expanded, -line, -linestop, -lineanchor, -nocase, -start, or --"
}
));
c.emit(Op::Extended(base_ext::ERROR, 0), -1);
c.push_empty();
return Ok(());
}
}
}
let rest = &args[i..];
let (fixed, max_vars) = if regsub { (3, 1) } else { (2, usize::MAX) };
if rest.len() < fixed || rest.len() - fixed > max_vars {
return c.error(format!("wrong # args: should be \"{usage}\""));
}
let vars = &rest[fixed..];
if flags & F_INLINE != 0 && !vars.is_empty() {
return c.error("regexp match variables not allowed when using -inline");
}
if regsub && !vars.is_empty() {
flags |= F_INTO_VAR;
}
let var_names = vars
.iter()
.map(|word| c.var_name_of(word))
.collect::<Result<Vec<_>, _>>()?;
c.emit(Op::LoadInt(flags), 1);
match start {
Some(word) => c.word(word)?,
None => {
c.emit(Op::LoadInt(0), 1);
}
}
for word in &rest[..fixed] {
c.word(word)?;
}
for name in &var_names {
let encoded = c.place_operand(name);
c.emit(Op::LoadInt(encoded), 1);
}
let operands = 2 + fixed + vars.len();
let Ok(argc) = u8::try_from(operands) else {
return c.error("too many match variables");
};
let id = if regsub { ext::REGSUB } else { ext::REGEXP };
c.emit(Op::Extended(id, argc), 1 - operands as i32);
Ok(())
}
fn translate(are: &str, flags: i64) -> Result<String, String> {
if let Some(literal) = are.strip_prefix("***=") {
return Ok(format!("{}{}", prefix(flags), regex::escape(literal)));
}
let body = are.strip_prefix("***:").unwrap_or(are);
let mut out = String::with_capacity(body.len() + 8);
let chars: Vec<char> = body.chars().collect();
let mut i = 0;
let mut in_class = false;
let mut quantifiers = 0u8;
let mut after_open = false;
while i < chars.len() {
let ch = chars[i];
if in_class {
if ch == '[' && matches!(chars.get(i + 1), Some('.') | Some('=')) {
return Err(refusal(if chars[i + 1] == '.' {
"a collating element ([. .])"
} else {
"an equivalence class ([= =])"
}));
}
if ch == ']' {
in_class = false;
quantifiers = 0;
}
out.push(ch);
i += 1;
continue;
}
let opened = std::mem::take(&mut after_open);
match ch {
'*' | '+' | '?' if !(ch == '?' && opened) => {
let allowed = if ch == '?' { 1 } else { 0 };
if quantifiers > allowed {
return Err(quantifier_operand());
}
quantifiers += 1;
out.push(ch);
i += 1;
}
'{' if is_bound(&chars[i..]) => {
if quantifiers > 0 {
return Err(quantifier_operand());
}
let Some(close) = chars[i..].iter().position(|&c| c == '}') else {
out.push(ch);
i += 1;
continue;
};
out.extend(&chars[i..=i + close]);
i += close + 1;
quantifiers = 1;
}
'[' => {
in_class = true;
quantifiers = 0;
out.push(ch);
i += 1;
if chars.get(i) == Some(&'^') {
out.push('^');
i += 1;
}
if chars.get(i) == Some(&']') {
out.push_str("\\]");
i += 1;
}
}
'(' => {
match chars.get(i + 1) {
Some('?') if matches!(chars.get(i + 2), Some('=') | Some('!')) => {
return Err(refusal("look-ahead ((?= ) or (?! ))"));
}
Some('?') if chars.get(i + 2) == Some(&'<') => {
return Err(refusal("look-behind ((?< ))"));
}
_ => {}
}
quantifiers = 0;
after_open = true;
out.push(ch);
i += 1;
}
'{' if !is_bound(&chars[i..]) => {
quantifiers = 0;
out.push_str("\\{");
i += 1;
}
'\\' => {
quantifiers = 0;
let Some(&next) = chars.get(i + 1) else {
out.push(ch);
i += 1;
continue;
};
match next {
'1'..='9' => return Err(refusal("a back-reference (\\1 … \\9)")),
'y' => {
out.push_str("\\b");
i += 2;
continue;
}
'Y' => {
out.push_str("\\B");
i += 2;
continue;
}
'm' => return Err(refusal("a word-start boundary (\\m)")),
'M' => return Err(refusal("a word-end boundary (\\M)")),
'Z' => {
out.push_str("\\z");
i += 2;
continue;
}
_ => {
out.push(ch);
out.push(next);
i += 2;
continue;
}
}
}
_ => {
quantifiers = 0;
out.push(ch);
i += 1;
}
}
}
Ok(format!("{}{}", prefix(flags), out))
}
fn is_bound(chars: &[char]) -> bool {
chars.get(1).is_some_and(char::is_ascii_digit)
}
fn prefix(flags: i64) -> String {
let mut f = String::from("(?");
if flags & F_NOCASE != 0 {
f.push('i');
}
if flags & F_EXPANDED != 0 {
f.push('x');
}
if flags & F_LINEANCHOR != 0 {
f.push('m');
}
if flags & F_LINESTOP == 0 {
f.push('s');
}
if f == "(?" {
return String::new();
}
f.push(')');
f
}
fn quantifier_operand() -> String {
"cannot compile regular expression pattern: invalid quantifier operand".to_string()
}
fn refusal(what: &str) -> String {
format!("{what} is not supported yet: the regular expression engine here matches in linear time, which back-references and look-around cannot")
}
thread_local! {
static CACHE: RefCell<HashMap<(i64, String), Arc<Regex>>> = RefCell::new(HashMap::new());
}
const CACHE_CAPACITY: usize = 1024;
fn regerror(detail: &str) -> &str {
match detail {
"unclosed character class" => "brackets [] not balanced",
"unclosed group" | "unopened group" => "parentheses () not balanced",
"repetition operator missing expression" => "invalid quantifier operand",
"invalid repetition count range, the start must be <= the end" => {
"invalid repetition count(s)"
}
"invalid character class range, the start must be <= the end" => "invalid character range",
"expected flag but got end of regex" => "invalid embedded option",
"unclosed counted repetition" => "braces {} not balanced",
other => other,
}
}
fn compiled(are: &str, flags: i64) -> Result<Arc<Regex>, String> {
let key = (flags, are.to_string());
if let Some(re) = CACHE.with(|cache| cache.borrow().get(&key).map(Arc::clone)) {
return Ok(re);
}
let translated = translate(are, flags)?;
CACHE.with(|cache| {
let re = Regex::new(&translated).map_err(|e| {
let detail = e.to_string();
let first = detail
.lines()
.find(|l| l.trim_start().starts_with("error:"))
.map(|l| l.trim_start().trim_start_matches("error:").trim())
.unwrap_or("syntax error");
format!(
"cannot compile regular expression pattern: {}",
regerror(first)
)
})?;
let re = Arc::new(re);
let mut cache = cache.borrow_mut();
if cache.len() >= CACHE_CAPACITY {
cache.clear();
}
cache.insert(key, Arc::clone(&re));
Ok(re)
})
}
struct CharIndex {
byte_of: Vec<usize>,
}
impl CharIndex {
fn new(s: &str) -> CharIndex {
let mut byte_of: Vec<usize> = s.char_indices().map(|(b, _)| b).collect();
byte_of.push(s.len());
CharIndex { byte_of }
}
fn char_at(&self, byte: usize) -> usize {
match self.byte_of.binary_search(&byte) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
}
}
fn byte_at(&self, ch: usize) -> usize {
*self
.byte_of
.get(ch)
.unwrap_or(self.byte_of.last().unwrap_or(&0))
}
fn chars(&self) -> usize {
self.byte_of.len() - 1
}
}
pub(crate) fn matches_anywhere(pattern: &str, subject: &str, nocase: bool) -> Result<bool, String> {
let flags = if nocase { F_NOCASE } else { 0 };
Ok(compiled(pattern, flags)?.is_match(subject))
}
pub(crate) fn extension(vm: &mut VM, id: u16, argc: u8) -> Result<(), String> {
let mut operands = Vec::with_capacity(argc as usize);
for _ in 0..argc {
operands.push(vm.pop());
}
operands.reverse();
match id {
ext::REGEXP => run_regexp(vm, &operands),
ext::REGSUB => run_regsub(None, vm, &operands),
ext::SWITCH_VARS => run_switch_vars(vm, &operands),
ext::SWITCH_CLEAR => run_switch_clear(vm, &operands),
other => Err(format!("unknown regexp op {other}")),
}
}
fn run_switch_vars(vm: &mut VM, operands: &[Value]) -> Result<(), String> {
let subject = to_tcl_string(operands.first().unwrap_or(&Value::Undef));
let pattern = to_tcl_string(operands.get(1).unwrap_or(&Value::Undef));
let nocase = matches!(operands.get(2), Some(Value::Int(1)));
let given = match operands.get(3) {
Some(Value::Int(g)) => *g,
_ => 0,
};
let re = compiled(&pattern, if nocase { F_NOCASE } else { 0 })?;
let Some(caps) = re.captures(&subject) else {
vm.push(Value::Str(Arc::new("0".to_string())));
return Ok(());
};
let idx = CharIndex::new(&subject);
if given & 1 != 0 {
let texts: Vec<String> = (0..caps.len())
.map(|g| match caps.get(g) {
Some(m) => subject[m.start()..m.end()].to_string(),
None => String::new(),
})
.collect();
let place = operands.get(4).ok_or("switch: no -matchvar place")?;
assign(vm, place, crate::list::join(&texts))?;
}
if given & 2 != 0 {
let pairs: Vec<String> = (0..caps.len())
.map(|g| switch_indices(caps.get(g).map(|m| (m.start(), m.end())), &idx))
.collect();
let place = operands.get(5).ok_or("switch: no -indexvar place")?;
assign(vm, place, crate::list::join(&pairs))?;
}
vm.push(Value::Str(Arc::new("1".to_string())));
Ok(())
}
fn switch_indices(span: Option<(usize, usize)>, idx: &CharIndex) -> String {
match span {
Some((s, e)) if idx.char_at(e) > 0 => {
format!("{} {}", idx.char_at(s), idx.char_at(e) as i64 - 1)
}
_ => "-1 -1".to_string(),
}
}
fn run_switch_clear(vm: &mut VM, operands: &[Value]) -> Result<(), String> {
let given = match operands.first() {
Some(Value::Int(g)) => *g,
_ => 0,
};
if given & 1 != 0 {
let place = operands.get(1).ok_or("switch: no -matchvar place")?.clone();
assign(vm, &place, String::new())?;
}
if given & 2 != 0 {
let place = operands.get(2).ok_or("switch: no -indexvar place")?.clone();
assign(vm, &place, String::new())?;
}
Ok(())
}
pub(crate) fn regsub_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), String> {
let mut operands = Vec::with_capacity(argc as usize);
for _ in 0..argc {
operands.push(vm.pop());
}
operands.reverse();
run_regsub(Some(interp), vm, &operands)
}
fn head(operands: &[Value]) -> Result<(i64, i64, String, String), String> {
let flags = match operands.first() {
Some(Value::Int(f)) => *f,
_ => return Err("regexp: switches missing".to_string()),
};
let start = match operands.get(1) {
Some(Value::Int(n)) => *n,
Some(other) => crate::list::wide(&to_tcl_string(other))?,
None => 0,
};
let pattern = to_tcl_string(operands.get(2).unwrap_or(&Value::Undef));
let subject = to_tcl_string(operands.get(3).unwrap_or(&Value::Undef));
Ok((flags, start, pattern, subject))
}
fn assign(vm: &mut VM, encoded: &Value, value: String) -> Result<(), String> {
let raw = match encoded {
Value::Int(v) => *v,
other => return Err(format!("regexp: not a variable place: {other:?}")),
};
let place = place_at(&Value::Int(raw >> 1), raw & 1 == 1)?;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::new(value));
}
Ok(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Stop {
BeforeEnd,
PastEnd,
EachCharacter,
}
fn matches<'s>(
re: &Regex,
subject: &'s str,
from: usize,
idx: &CharIndex,
stop: Stop,
) -> Vec<regex::Captures<'s>> {
let len = subject.len();
let mut found = Vec::new();
let mut pos = from;
if stop == Stop::EachCharacter && len == 0 {
return found;
}
while let Some(caps) = re.captures_at(subject, pos) {
let whole = caps.get(0).expect("group 0 always participates");
let (s, e) = (whole.start(), whole.end());
found.push(caps);
pos = if e > s {
e
} else if s >= len {
len + 1
} else {
let ch = idx.char_at(s);
idx.byte_at(ch + 1)
};
match stop {
Stop::PastEnd if pos > len => break,
Stop::BeforeEnd | Stop::EachCharacter if pos >= len => break,
_ => {}
}
}
found
}
fn indices(span: Option<(usize, usize)>, idx: &CharIndex) -> String {
match span {
Some((s, e)) if e > s => format!("{} {}", idx.char_at(s), idx.char_at(e) - 1),
Some((s, _)) => format!("{} {}", idx.char_at(s), idx.char_at(s) as i64 - 1),
None => "-1 -1".to_string(),
}
}
fn run_regexp(vm: &mut VM, operands: &[Value]) -> Result<(), String> {
let (flags, start, pattern, subject) = head(operands)?;
let places = &operands[4.min(operands.len())..];
let re = compiled(&pattern, flags)?;
let idx = CharIndex::new(&subject);
let from_char = start.max(0) as usize;
if from_char > idx.chars() {
return finish_no_match(vm, flags, places);
}
let from_byte = idx.byte_at(from_char);
let all = flags & F_ALL != 0;
let found = if all {
matches(&re, &subject, from_byte, &idx, Stop::BeforeEnd)
} else {
re.captures_at(&subject, from_byte).into_iter().collect()
};
let count = found.len() as i64;
let mut inline: Vec<String> = Vec::new();
if flags & F_INLINE != 0 {
for caps in &found {
for g in 0..caps.len() {
let span = caps.get(g).map(|m| (m.start(), m.end()));
inline.push(if flags & F_INDICES != 0 {
indices(span, &idx)
} else {
span.map(|(s, e)| subject[s..e].to_string())
.unwrap_or_default()
});
}
}
}
let last = found.into_iter().next_back();
if flags & F_INLINE != 0 {
vm.push(Value::Str(Arc::new(crate::list::join(&inline))));
return Ok(());
}
let Some(caps) = last else {
return finish_no_match(vm, flags, places);
};
for (i, place) in places.iter().enumerate() {
let span = caps.get(i).map(|m| (m.start(), m.end()));
let text = if flags & F_INDICES != 0 {
indices(span, &idx)
} else {
span.map(|(s, e)| subject[s..e].to_string())
.unwrap_or_default()
};
assign(vm, place, text)?;
}
vm.push(Value::Int(if flags & F_ALL != 0 { count } else { 1 }));
Ok(())
}
fn finish_no_match(vm: &mut VM, flags: i64, _places: &[Value]) -> Result<(), String> {
if flags & F_INLINE != 0 {
vm.push(Value::Str(Arc::new(String::new())));
} else {
vm.push(Value::Int(0));
}
Ok(())
}
fn run_regsub(interp: Option<&Shared>, vm: &mut VM, operands: &[Value]) -> Result<(), String> {
let (flags, start, pattern, subject) = head(operands)?;
let spec = to_tcl_string(operands.get(4).unwrap_or(&Value::Undef));
let re = compiled(&pattern, flags)?;
let idx = CharIndex::new(&subject);
let from_char = start.max(0) as usize;
let from_byte = if from_char > idx.chars() {
subject.len()
} else {
idx.byte_at(from_char)
};
let found = if flags & F_ALL != 0 {
matches(
&re,
&subject,
from_byte,
&idx,
if pattern.is_empty() {
Stop::EachCharacter
} else {
Stop::PastEnd
},
)
} else {
re.captures_at(&subject, from_byte).into_iter().collect()
};
let replacements = if flags & F_COMMAND != 0 {
Some(call_replacements(interp, vm, &spec, &found, &subject)?)
} else {
None
};
let mut out = String::with_capacity(subject.len());
let mut count: i64 = 0;
let mut cursor = 0usize;
for (n, caps) in found.iter().enumerate() {
let whole = caps.get(0).expect("group 0 always participates");
out.push_str(&subject[cursor..whole.start()]);
match &replacements {
Some(results) => out.push_str(&results[n]),
None => expand(&spec, caps, &subject, &mut out),
}
cursor = whole.end();
count += 1;
}
out.push_str(&subject[cursor..]);
if flags & F_INTO_VAR != 0 {
let place = operands.last().ok_or("regsub: variable place missing")?;
assign(vm, place, out)?;
vm.push(Value::Int(count));
} else {
vm.push(Value::Str(Arc::new(out)));
}
Ok(())
}
fn call_replacements(
interp: Option<&Shared>,
vm: &mut VM,
spec: &str,
found: &[regex::Captures],
subject: &str,
) -> Result<Vec<String>, String> {
let prefix = crate::list::split(spec)?;
if prefix.is_empty() {
return Err("command prefix must be a list of at least one element".to_string());
}
let Some(interp) = interp else {
return Err("regsub -command needs an interpreter to call".to_string());
};
let calls: Vec<Vec<String>> = found
.iter()
.map(|caps| {
let mut words = prefix.clone();
for g in 0..caps.len() {
words.push(match caps.get(g) {
Some(m) => subject[m.start()..m.end()].to_string(),
None => String::new(),
});
}
words
})
.collect();
crate::runtime::at_global(interp, vm, |interp| {
calls
.iter()
.map(|words| {
crate::runtime::run_source(interp, &crate::list::join(words))
.map(|v| to_tcl_string(&v))
.map_err(|e| e.msg)
})
.collect()
})
}
fn expand(spec: &str, caps: ®ex::Captures, subject: &str, out: &mut String) {
let chars: Vec<char> = spec.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'&' => {
if let Some(m) = caps.get(0) {
out.push_str(&subject[m.start()..m.end()]);
}
i += 1;
}
'\\' => match chars.get(i + 1) {
Some(d @ '0'..='9') => {
let g = *d as usize - '0' as usize;
if let Some(m) = caps.get(g) {
out.push_str(&subject[m.start()..m.end()]);
}
i += 2;
}
Some(&other) => {
out.push(other);
i += 2;
}
None => {
out.push('\\');
i += 1;
}
},
other => {
out.push(other);
i += 1;
}
}
}
}