use std::sync::Arc;
use fusevm::{Op, Value, VM};
use num_bigint::BigInt;
use crate::assoc::{target_of, Target};
use crate::compiler::{CompileError, Compiler};
use crate::list;
use crate::parser::Word;
use crate::runtime::{format_double, place_at, to_tcl_string, var_cell};
pub mod ext {
pub use crate::compiler::ext::STRING_BASE as BASE;
pub const SCAN: u16 = BASE + 40;
}
pub(crate) fn compile(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
if args.len() < 2 {
return c.error("wrong # args: should be \"scan string format ?varName ...?\"");
}
let vars = &args[2..];
let mut names = Vec::with_capacity(vars.len());
for w in vars {
match target_of(w) {
Some(Target::Scalar(name)) => names.push(name),
_ => return c.error("\"scan\" into an array element is not supported yet"),
}
}
c.word(&args[0])?;
c.word(&args[1])?;
for name in &names {
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.emit(Op::LoadInt(names.len() as i64), 1);
c.emit(Op::Extended(ext::SCAN, 0), -(3 * names.len() as i32 + 2));
Ok(())
}
pub(crate) fn extension(vm: &mut VM) -> Result<(), String> {
let count = to_tcl_string(&vm.pop())
.parse::<usize>()
.expect("the variable count is emitted as an integer");
let mut places = Vec::with_capacity(count);
for _ in 0..count {
let operand = vm.pop();
let in_frame = to_tcl_string(&vm.pop()) == "1";
let _name = vm.pop();
places.push(place_at(&operand, in_frame)?);
}
places.reverse();
let format = to_tcl_string(&vm.pop());
let subject = to_tcl_string(&vm.pop());
let total = validate(&format, count)?;
let scanned = run(&subject, &format, total)?;
if count == 0 {
let values: Vec<String> = if scanned.underflow && scanned.conversions == 0 {
Vec::new()
} else {
scanned
.values
.into_iter()
.map(Option::unwrap_or_default)
.collect()
};
vm.push(Value::Str(Arc::new(list::join(&values))));
return Ok(());
}
let mut assigned = 0i64;
for (place, value) in places.into_iter().zip(scanned.values) {
let Some(value) = value else { continue };
assigned += 1;
if let Some(cell) = var_cell(vm, place) {
*cell = Value::Str(Arc::new(value));
}
}
vm.push(Value::Int(
if scanned.underflow && scanned.conversions == 0 {
-1
} else {
assigned
},
));
Ok(())
}
struct Scanned {
values: Vec<Option<String>>,
conversions: usize,
underflow: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Size {
Int,
Wide,
Big,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Radix {
Decimal,
Prefixed,
Octal,
Hex,
Binary,
}
struct Spec {
suppress: bool,
index: usize,
width: usize,
size: Size,
kind: char,
set: Option<CharSet>,
}
fn validate(format: &str, num_vars: usize) -> Result<usize, String> {
let f: Vec<char> = format.chars().collect();
let mut i = 0;
let mut obj_index = 0usize;
let mut got_xpg = false;
let mut got_sequential = false;
let mut xpg_size = 0usize;
let mut assigned: Vec<usize> = vec![0; num_vars];
while i < f.len() {
if f[i] != '%' {
i += 1;
continue;
}
i += 1;
if i < f.len() && f[i] == '%' {
i += 1;
continue;
}
let mut suppress = false;
let mut has_width = false;
if i < f.len() && f[i] == '*' {
suppress = true;
i += 1;
} else if let Some((value, end)) = digits_at(&f, i) {
if f.get(end) == Some(&'$') {
i = end + 1;
got_xpg = true;
if got_sequential {
return Err("cannot mix \"%\" and \"%n$\" conversion specifiers".to_string());
}
if value == 0 || value >= i64::from(i32::MAX) as u64 {
return Err(bad_index(got_xpg));
}
obj_index = value as usize - 1;
if num_vars > 0 && obj_index >= num_vars {
return Err(bad_index(got_xpg));
}
if num_vars == 0 {
xpg_size = xpg_size.max(value as usize);
}
} else {
got_sequential = true;
if got_xpg {
return Err("cannot mix \"%\" and \"%n$\" conversion specifiers".to_string());
}
}
} else {
got_sequential = true;
if got_xpg {
return Err("cannot mix \"%\" and \"%n$\" conversion specifiers".to_string());
}
}
if let Some((width, end)) = digits_at(&f, i) {
if width >= i64::MAX as u64 {
return Err(format!(
"specified field width {width} exceeds limit {}.",
i64::MAX - 1
));
}
has_width = true;
i = end;
}
let size = read_size(&f, &mut i);
if !suppress && num_vars > 0 && obj_index >= num_vars {
return Err(bad_index(got_xpg));
}
let ch = f.get(i).copied().unwrap_or('\0');
i += 1;
match ch {
'c' if has_width => {
return Err("field width may not be specified in %c conversion".to_string())
}
'c' | 'n' | 's' if size != Size::Int => {
return Err(format!(
"field size modifier may not be specified in %{ch} conversion"
))
}
'c' | 'n' | 's' | 'd' | 'e' | 'E' | 'f' | 'g' | 'G' | 'i' | 'o' | 'x' | 'X' | 'b'
| 'u' => {}
'[' => {
if size != Size::Int {
return Err(format!(
"field size modifier may not be specified in %{ch} conversion"
));
}
i = skip_char_set(&f, i).ok_or("unmatched [ in format string")?;
}
other => return Err(format!("bad scan conversion character \"{other}\"")),
}
if !suppress {
if obj_index >= assigned.len() {
assigned.resize(obj_index + 1, 0);
}
assigned[obj_index] += 1;
obj_index += 1;
}
}
let total = if num_vars == 0 {
if xpg_size > 0 {
xpg_size
} else {
obj_index
}
} else {
num_vars
};
for slot in assigned.iter().take(total) {
if *slot > 1 {
return Err(
"variable is assigned by multiple \"%n$\" conversion specifiers".to_string(),
);
}
if xpg_size == 0 && *slot == 0 {
return Err("variable is not assigned by any conversion specifiers".to_string());
}
}
Ok(total)
}
fn bad_index(got_xpg: bool) -> String {
if got_xpg {
"\"%n$\" argument index out of range".to_string()
} else {
"different numbers of variable names and field specifiers".to_string()
}
}
fn digits_at(f: &[char], at: usize) -> Option<(u64, usize)> {
let mut end = at;
while f.get(end).is_some_and(char::is_ascii_digit) {
end += 1;
}
if end == at {
return None;
}
let value = f[at..end]
.iter()
.collect::<String>()
.parse::<u64>()
.unwrap_or(u64::MAX);
Some((value, end))
}
fn read_size(f: &[char], at: &mut usize) -> Size {
match f.get(*at) {
Some('z' | 't' | 'j' | 'q') => {
*at += 1;
Size::Wide
}
Some('L') => {
*at += 1;
Size::Big
}
Some('l') => {
*at += 1;
if f.get(*at) == Some(&'l') {
*at += 1;
Size::Big
} else {
Size::Wide
}
}
Some('h') => {
*at += 1;
Size::Int
}
_ => Size::Int,
}
}
fn skip_char_set(f: &[char], at: usize) -> Option<usize> {
let mut i = at;
if f.get(i) == Some(&'^') {
i += 1;
}
if f.get(i) == Some(&']') {
i += 1;
}
while f.get(i) != Some(&']') {
f.get(i)?;
i += 1;
}
Some(i + 1)
}
struct CharSet {
exclude: bool,
chars: Vec<char>,
ranges: Vec<(char, char)>,
}
impl CharSet {
fn build(f: &[char], at: &mut usize) -> CharSet {
let mut set = CharSet {
exclude: false,
chars: Vec::new(),
ranges: Vec::new(),
};
if f.get(*at) == Some(&'^') {
set.exclude = true;
*at += 1;
}
let has_range = {
let mut i = *at;
if f.get(i) == Some(&']') {
i += 1;
}
let mut found = false;
while let Some(&c) = f.get(i) {
if c == ']' {
break;
}
if c == '-' {
found = true;
}
i += 1;
}
found
};
let mut start = *f.get(*at).unwrap_or(&']');
if start == ']' || start == '-' {
set.chars.push(start);
*at += 1;
}
while let Some(&ch) = f.get(*at) {
if ch == ']' {
break;
}
if f.get(*at + 1) == Some(&'-') {
start = ch;
} else if ch == '-' {
if f.get(*at + 1) == Some(&']') || !has_range {
set.chars.push(start);
set.chars.push(ch);
} else {
*at += 1;
let end = *f.get(*at).unwrap_or(&']');
set.ranges.push(if start < end {
(start, end)
} else {
(end, start)
});
}
} else {
set.chars.push(ch);
}
*at += 1;
}
*at += 1;
set
}
fn holds(&self, c: char) -> bool {
let found = self.chars.contains(&c)
|| self
.ranges
.iter()
.any(|&(start, end)| start <= c && c <= end);
found != self.exclude
}
}
fn run(subject: &str, format: &str, total: usize) -> Result<Scanned, String> {
let s: Vec<char> = subject.chars().collect();
let f: Vec<char> = format.chars().collect();
let mut values: Vec<Option<String>> = (0..total).map(|_| None).collect();
let mut conversions = 0usize;
let mut underflow = false;
let mut at = 0usize; let mut i = 0usize; let mut next_slot = 0usize;
'format: while i < f.len() {
let ch = f[i];
i += 1;
if ch.is_whitespace() {
while s.get(at).is_some_and(|c| c.is_whitespace()) {
at += 1;
}
continue;
}
let mut literal = ch;
if ch == '%' {
let Some(&next) = f.get(i) else {
break;
};
i += 1;
if next != '%' {
let spec = read_spec(&f, &mut i, &mut next_slot);
match convert(&s, &mut at, &spec, &mut values) {
Outcome::Converted => {
conversions += 1;
continue;
}
Outcome::Stopped => break 'format,
Outcome::Underflow => {
underflow = true;
break 'format;
}
}
}
literal = next;
}
let Some(&got) = s.get(at) else {
underflow = true;
break;
};
at += 1;
if literal != got {
break;
}
}
Ok(Scanned {
values,
conversions,
underflow,
})
}
fn read_spec(f: &[char], i: &mut usize, next_slot: &mut usize) -> Spec {
let mut spec = Spec {
suppress: false,
index: *next_slot,
width: 0,
size: Size::Int,
kind: '\0',
set: None,
};
*i -= 1;
if f.get(*i) == Some(&'*') {
spec.suppress = true;
*i += 1;
} else if let Some((value, end)) = digits_at(f, *i) {
if f.get(end) == Some(&'$') {
*i = end + 1;
spec.index = value as usize - 1;
}
}
if let Some((width, end)) = digits_at(f, *i) {
spec.width = width as usize;
*i = end;
}
spec.size = read_size(f, i);
spec.kind = *f.get(*i).unwrap_or(&'\0');
*i += 1;
if spec.kind == '[' {
spec.set = Some(CharSet::build(f, i));
}
if !spec.suppress {
*next_slot = spec.index + 1;
}
spec
}
enum Outcome {
Converted,
Stopped,
Underflow,
}
fn convert(s: &[char], at: &mut usize, spec: &Spec, values: &mut [Option<String>]) -> Outcome {
let mut store = |text: String| {
if !spec.suppress {
if let Some(slot) = values.get_mut(spec.index) {
*slot = Some(text);
}
}
};
if spec.kind == 'n' {
store(at.to_string());
return Outcome::Converted;
}
if *at >= s.len() {
return Outcome::Underflow;
}
if !matches!(spec.kind, 'c' | '[') {
while s.get(*at).is_some_and(|c| c.is_whitespace()) {
*at += 1;
}
if *at >= s.len() {
return Outcome::Underflow;
}
}
let width = if spec.width == 0 {
usize::MAX
} else {
spec.width
};
match spec.kind {
's' => {
let start = *at;
while *at < s.len() && !s[*at].is_whitespace() && *at - start < width {
*at += 1;
}
store(s[start..*at].iter().collect());
Outcome::Converted
}
'[' => {
let set = spec.set.as_ref().expect("a set conversion carries one");
let start = *at;
while *at < s.len() && set.holds(s[*at]) && *at - start < width {
*at += 1;
}
if *at == start {
return Outcome::Stopped;
}
store(s[start..*at].iter().collect());
Outcome::Converted
}
'c' => {
let c = s[*at];
*at += 1;
store((c as u32).to_string());
Outcome::Converted
}
'f' | 'e' | 'E' | 'g' | 'G' => match parse_double(s, *at, width) {
Ok((value, _)) if value.is_nan() => Outcome::Stopped,
Ok((value, end)) => {
*at = end;
store(format_double(value));
Outcome::Converted
}
Err(reached) => underflow_or_stop(s, reached, *at, spec.width),
},
_ => {
let radix = match spec.kind {
'i' => Radix::Prefixed,
'o' => Radix::Octal,
'x' | 'X' => Radix::Hex,
'b' => Radix::Binary,
_ => Radix::Decimal,
};
match parse_integer(s, *at, width, radix) {
Ok((value, end)) => {
*at = end;
store(narrow(value, spec.size, spec.kind == 'u'));
Outcome::Converted
}
Err(reached) => underflow_or_stop(s, reached, *at, spec.width),
}
}
}
}
fn underflow_or_stop(s: &[char], reached: usize, at: usize, width: usize) -> Outcome {
let ran_out = if width == 0 {
reached >= s.len()
} else {
reached == at + width
};
if ran_out {
Outcome::Underflow
} else {
Outcome::Stopped
}
}
fn narrow(value: BigInt, size: Size, unsigned: bool) -> String {
if size == Size::Big {
return value.to_string();
}
let fitted = match size {
Size::Int => match i64::try_from(&value) {
Ok(w) if (i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&w) => {
i64::from(w as i32)
}
_ => {
if value.sign() == num_bigint::Sign::Minus {
i64::from(i32::MIN)
} else {
i64::from(i32::MAX)
}
}
},
_ => i64::try_from(&value).unwrap_or({
if value.sign() == num_bigint::Sign::Minus {
i64::MIN
} else {
i64::MAX
}
}),
};
if unsigned && fitted < 0 {
(fitted as u64).to_string()
} else {
fitted.to_string()
}
}
fn parse_integer(
s: &[char],
at: usize,
width: usize,
radix: Radix,
) -> Result<(BigInt, usize), usize> {
let limit = s.len().min(at.saturating_add(width));
let peek = |k: usize| if k < limit { s.get(k).copied() } else { None };
let mut i = at;
let mut negative = false;
match peek(i) {
Some('+') => i += 1,
Some('-') => {
negative = true;
i += 1;
}
_ => {}
}
let zero_led = peek(i) == Some('0');
let (base, start) = match radix {
Radix::Decimal => (10, i),
Radix::Octal => (8, i),
Radix::Hex if zero_led && matches!(peek(i + 1), Some('x' | 'X')) => (16, i + 2),
Radix::Hex => (16, i),
Radix::Binary if zero_led && matches!(peek(i + 1), Some('b' | 'B')) => (2, i + 2),
Radix::Binary => (2, i),
Radix::Prefixed if zero_led && matches!(peek(i + 1), Some('x' | 'X')) => (16, i + 2),
Radix::Prefixed if zero_led => (8, i),
Radix::Prefixed => (10, i),
};
let mut end = start;
while end < limit && s[end].is_digit(base) {
end += 1;
}
if end == start {
return if start > i {
Ok((BigInt::from(0), i + 1))
} else {
Err(i)
};
}
let digits: String = s[start..end].iter().collect();
let magnitude = BigInt::parse_bytes(digits.as_bytes(), base).ok_or(i)?;
Ok((if negative { -magnitude } else { magnitude }, end))
}
fn parse_double(s: &[char], at: usize, width: usize) -> Result<(f64, usize), usize> {
let limit = s.len().min(at.saturating_add(width));
let mut i = at;
if i < limit && matches!(s.get(i), Some('+' | '-')) {
i += 1;
}
let reached = if s.get(i) == Some(&'.') && i < limit {
i + 1
} else {
i
};
if let Some(end) = word_at(s, i, limit, "infinity").or_else(|| word_at(s, i, limit, "inf")) {
let text: String = s[at..end].iter().collect();
return Ok((
if text.starts_with('-') {
f64::NEG_INFINITY
} else {
f64::INFINITY
},
end,
));
}
if let Some(end) = word_at(s, i, limit, "nan") {
return Ok((f64::NAN, end));
}
let mut accepted = None;
let mut digits = 0;
while i < limit && s[i].is_ascii_digit() {
i += 1;
digits += 1;
}
if digits > 0 {
accepted = Some(i);
}
if i < limit && s[i] == '.' {
let mut j = i + 1;
let mut fraction = 0;
while j < limit && s[j].is_ascii_digit() {
j += 1;
fraction += 1;
}
if digits > 0 || fraction > 0 {
i = j;
accepted = Some(i);
}
}
let Some(whole) = accepted else {
return Err(reached);
};
if i < limit && matches!(s[i], 'e' | 'E') {
let mut j = i + 1;
if j < limit && matches!(s[j], '+' | '-') {
j += 1;
}
let mut exponent = 0;
while j < limit && s[j].is_ascii_digit() {
j += 1;
exponent += 1;
}
if exponent > 0 {
accepted = Some(j);
}
}
let end = accepted.unwrap_or(whole);
let text: String = s[at..end].iter().collect();
let mut value = text.parse::<f64>().unwrap_or(if text.starts_with('-') {
f64::NEG_INFINITY
} else {
f64::INFINITY
});
if value == 0.0 && !text.contains(['.', 'e', 'E']) {
value = 0.0;
}
Ok((value, end))
}
fn word_at(s: &[char], i: usize, limit: usize, word: &str) -> Option<usize> {
let mut at = i;
for want in word.chars() {
if at >= limit || s[at].to_ascii_lowercase() != want {
return None;
}
at += 1;
}
Some(at)
}