use super::{Word, WordPart};
pub fn eval_word(word: &Word) -> String {
let mut out = String::new();
for part in &word.0 {
eval_part(part, &mut out);
}
out
}
const BRACE_EXPANSION_CAP: usize = 256;
const UNPINNABLE: &str = "__SAFE_CHAINS_CMDSUB__";
pub(crate) const TAGGED_PREFIX: &str = "__SAFE_CHAINS_CMDSUB_";
pub(crate) const ATOM_SENTINEL: &str = "__SAFE_CHAINS_CMDSUB_ATOM__";
pub fn expand_word(word: &Word) -> Vec<String> {
let mut acc = vec![String::new()];
for part in &word.0 {
let alts: Vec<String> = match part {
WordPart::Lit(s) => brace_expand(s),
other => {
let mut t = String::new();
eval_part(other, &mut t);
vec![t]
}
};
if alts.len() > BRACE_EXPANSION_CAP || acc.len().saturating_mul(alts.len()) > BRACE_EXPANSION_CAP {
return vec![UNPINNABLE.to_string()];
}
if let [b] = alts.as_slice() {
for a in &mut acc {
a.push_str(b);
}
} else {
let mut next = Vec::with_capacity(acc.len() * alts.len());
for a in &acc {
for b in &alts {
next.push(format!("{a}{b}"));
}
}
acc = next;
}
}
let extra = u32::try_from(acc.len().saturating_sub(1)).unwrap_or(u32::MAX);
if extra > 0 && !crate::cst::check::charge_classify_work(extra) {
return vec![UNPINNABLE.to_string()];
}
acc
}
fn brace_expand(s: &str) -> Vec<String> {
if s.matches('{').count() > 8 {
return vec![UNPINNABLE.to_string()];
}
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '{' {
let mut depth = 1;
let mut j = i + 1;
let mut comma = false;
while j < chars.len() {
match chars[j] {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
break;
}
}
',' if depth == 1 => comma = true,
_ => {}
}
j += 1;
}
if j < chars.len() && depth == 0 && comma {
let prefix: String = chars[..i].iter().collect();
let content: String = chars[i + 1..j].iter().collect();
let suffix: String = chars[j + 1..].iter().collect();
let mut out = Vec::new();
for alt in split_top_commas(&content) {
for alt_x in brace_expand(&alt) {
for suf_x in brace_expand(&suffix) {
out.push(format!("{prefix}{alt_x}{suf_x}"));
}
}
}
return out;
}
}
i += 1;
}
vec![s.to_string()]
}
fn split_top_commas(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut cur = String::new();
for c in s.chars() {
match c {
'{' => {
depth += 1;
cur.push(c);
}
'}' => {
depth -= 1;
cur.push(c);
}
',' if depth == 0 => parts.push(std::mem::take(&mut cur)),
_ => cur.push(c),
}
}
parts.push(cur);
parts
}
fn sub_sentinel(inner: &super::Script) -> String {
use crate::engine::resolve::SubClaim;
match crate::engine::resolve::substitution_claim(inner) {
Some(SubClaim::Locus(locus)) => {
use crate::engine::facet::FacetTerm;
format!("{TAGGED_PREFIX}{}__", locus.as_str().to_uppercase().replace('-', "_"))
}
Some(SubClaim::Atom) => ATOM_SENTINEL.to_string(),
None => UNPINNABLE.to_string(),
}
}
fn eval_part(part: &WordPart, out: &mut String) {
match part {
WordPart::Lit(s) => out.push_str(s),
WordPart::Escape(c) => out.push(*c),
WordPart::SQuote(s) => out.push_str(s),
WordPart::DQuote(inner) => {
for p in &inner.0 {
eval_part(p, out);
}
}
WordPart::CmdSub(inner) => out.push_str(&sub_sentinel(inner)),
WordPart::Backtick(raw) => match super::parse(raw) {
Some(inner) => out.push_str(&sub_sentinel(&inner)),
None => out.push_str(UNPINNABLE),
},
WordPart::ProcSub(_) => out.push_str("__SAFE_CHAINS_PROCSUB__"),
WordPart::Arith(_) => out.push_str("__SAFE_CHAINS_ARITH__"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal() {
let w = Word(vec![WordPart::Lit("hello".into())]);
assert_eq!(w.eval(), "hello");
}
#[test]
fn single_quoted() {
let w = Word(vec![WordPart::SQuote("hello world".into())]);
assert_eq!(w.eval(), "hello world");
}
#[test]
fn double_quoted() {
let w = Word(vec![WordPart::DQuote(Word(vec![
WordPart::Lit("hello ".into()),
WordPart::Lit("world".into()),
]))]);
assert_eq!(w.eval(), "hello world");
}
#[test]
fn mixed_parts() {
let w = Word(vec![
WordPart::Lit("foo".into()),
WordPart::DQuote(Word(vec![WordPart::Lit("bar".into())])),
WordPart::SQuote("baz".into()),
]);
assert_eq!(w.eval(), "foobarbaz");
}
#[test]
fn cmd_sub_placeholder() {
let w = Word(vec![WordPart::CmdSub(super::super::Script(vec![]))]);
assert_eq!(w.eval(), "__SAFE_CHAINS_CMDSUB__");
}
#[test]
fn dquote_with_escape() {
let w = Word(vec![WordPart::DQuote(Word(vec![
WordPart::Lit("hello".into()),
WordPart::Escape('"'),
WordPart::Lit("world".into()),
]))]);
assert_eq!(w.eval(), "hello\"world");
}
}