use std::collections::HashMap;
use std::rc::Rc;
use super::ast::*;
use super::builtins;
use super::host::{Host, ObjRef};
use super::value::{self, ArithMode, Operand, VResult, Variant, VbaError};
const DEFAULT_MAX_OPS: u64 = 5_000_000;
const DEFAULT_MAX_DEPTH: usize = 64;
fn out_of_scope(what: &str) -> VbaError {
VbaError::new(
438,
format!("Object doesn't support this property or method: {what} is not available"),
)
}
fn needs_workbook(what: &str) -> VbaError {
VbaError::new(
438,
format!(
"Object doesn't support this property or method: {what} needs a workbook, and this run has none"
),
)
}
#[derive(Debug, Clone, PartialEq)]
enum Flow {
Normal,
ExitProc,
ExitFor,
ExitDo,
Goto(String),
}
#[derive(Debug, Clone, PartialEq)]
enum Handler {
None,
ResumeNext,
Goto(String),
}
struct Frame {
locals: HashMap<String, Variant>,
handler: Handler,
in_handler: bool,
failed_at: Option<usize>,
with_stack: Vec<Variant>,
}
impl Frame {
fn new() -> Self {
Self {
locals: HashMap::new(),
handler: Handler::None,
in_handler: false,
failed_at: None,
with_stack: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default)]
struct ErrState {
number: i32,
description: String,
}
pub struct Interpreter<'w> {
module: Module,
procs: HashMap<String, Rc<Procedure>>,
globals: HashMap<String, Variant>,
err: ErrState,
ops: u64,
max_ops: u64,
depth: usize,
max_depth: usize,
host: Option<Host<'w>>,
}
impl<'w> Interpreter<'w> {
pub fn new(module: Module) -> Self {
let procs = module
.procedures()
.into_iter()
.map(|p| (p.name.to_ascii_lowercase(), Rc::new(p.clone())))
.collect();
Self {
module,
procs,
globals: HashMap::new(),
err: ErrState::default(),
ops: 0,
max_ops: DEFAULT_MAX_OPS,
depth: 0,
max_depth: DEFAULT_MAX_DEPTH,
host: None,
}
}
pub fn with_host(mut self, host: Host<'w>) -> Self {
self.host = Some(host);
self
}
pub fn mutated(&self) -> bool {
self.host.as_ref().is_some_and(|h| h.mutated())
}
pub fn finish(&mut self) {
if let Some(h) = self.host.as_mut() {
h.finish();
}
}
fn host(&mut self, what: &str) -> VResult<&mut Host<'w>> {
self.host.as_mut().ok_or_else(|| needs_workbook(what))
}
pub fn with_max_ops(mut self, max_ops: u64) -> Self {
self.max_ops = max_ops;
self
}
pub fn run(&mut self, name: &str, args: Vec<Variant>) -> VResult<Variant> {
self.ops = 0;
self.init_module_level()?;
self.call_procedure(name, args)
}
fn init_module_level(&mut self) -> VResult<()> {
let items = std::mem::take(&mut self.module.items);
for item in &items {
if let ModuleItem::Declaration(stmt) = item {
let mut frame = Frame::new();
let r = self.exec_stmt(stmt, &mut frame, true);
for (k, v) in frame.locals {
self.globals.insert(k, v);
}
r?;
}
}
self.module.items = items;
Ok(())
}
fn find_procedure(&self, name: &str) -> Option<Rc<Procedure>> {
self.procs.get(&name.to_ascii_lowercase()).cloned()
}
fn call_procedure(&mut self, name: &str, args: Vec<Variant>) -> VResult<Variant> {
let Some(proc) = self.find_procedure(name) else {
return Err(VbaError::new(
35,
format!("Sub or Function not defined: {name}"),
));
};
self.depth += 1;
if self.depth > self.max_depth {
self.depth -= 1;
return Err(VbaError::new(28, "Out of stack space"));
}
let result = self.call_body(&proc, args);
self.depth -= 1;
result
}
fn call_body(&mut self, proc: &Procedure, args: Vec<Variant>) -> VResult<Variant> {
let mut frame = Frame::new();
for (i, param) in proc.params.iter().enumerate() {
let value = args.get(i).cloned().unwrap_or(Variant::Empty);
frame.locals.insert(param.name.to_ascii_lowercase(), value);
}
let ret_key = proc.name.to_ascii_lowercase();
if proc.kind != ProcKind::Sub {
frame
.locals
.entry(ret_key.clone())
.or_insert(Variant::Empty);
}
self.exec_procedure_body(&proc.body, &mut frame)?;
Ok(if proc.kind == ProcKind::Sub {
Variant::Empty
} else {
frame
.locals
.get(&ret_key)
.cloned()
.unwrap_or(Variant::Empty)
})
}
fn exec_procedure_body(&mut self, body: &[Stmt], frame: &mut Frame) -> VResult<()> {
let mut pc = 0usize;
while pc < body.len() {
let flow = match self.exec_stmt(&body[pc], frame, false) {
Ok(f) => f,
Err(e) => {
frame.failed_at = Some(pc);
match self.take_handler(frame) {
Handler::ResumeNext => {
self.set_err(&e);
pc += 1;
continue;
}
Handler::Goto(label) => {
self.set_err(&e);
frame.in_handler = true;
Flow::Goto(label)
}
Handler::None => return Err(e),
}
}
};
match flow {
Flow::Normal => pc += 1,
Flow::ExitProc => return Ok(()),
Flow::ExitFor | Flow::ExitDo => pc += 1,
Flow::Goto(label) => {
if label == "\0resume-next" {
pc = frame.failed_at.map(|i| i + 1).unwrap_or(pc + 1);
frame.in_handler = false;
continue;
}
match Self::find_label(body, &label) {
Some(i) => pc = i,
None => {
return Err(VbaError::new(
erl_label_error(),
format!("Label not defined: {label}"),
));
}
}
}
}
}
Ok(())
}
fn find_label(body: &[Stmt], label: &str) -> Option<usize> {
body.iter()
.position(|s| matches!(s, Stmt::Label { name, .. } if name.eq_ignore_ascii_case(label)))
}
fn take_handler(&self, frame: &Frame) -> Handler {
if frame.in_handler {
Handler::None
} else {
frame.handler.clone()
}
}
fn set_err(&mut self, e: &VbaError) {
self.err = ErrState {
number: e.number,
description: e.description.clone(),
};
}
fn exec_block(&mut self, body: &[Stmt], frame: &mut Frame) -> VResult<Flow> {
for stmt in body {
match self.exec_stmt(stmt, frame, false) {
Ok(Flow::Normal) => {}
Ok(other) => return Ok(other),
Err(e) => match self.take_handler(frame) {
Handler::ResumeNext => {
self.set_err(&e);
continue;
}
Handler::Goto(label) => {
self.set_err(&e);
frame.in_handler = true;
return Ok(Flow::Goto(label));
}
Handler::None => return Err(e),
},
}
}
Ok(Flow::Normal)
}
fn tick(&mut self) -> VResult<()> {
self.ops += 1;
if self.ops > self.max_ops {
return Err(VbaError::new(
16,
"Expression too complex: statement limit exceeded (possible infinite loop)",
));
}
Ok(())
}
fn exec_stmt(&mut self, stmt: &Stmt, frame: &mut Frame, module_level: bool) -> VResult<Flow> {
self.tick()?;
match stmt {
Stmt::Label { .. } => Ok(Flow::Normal),
Stmt::Dim { vars, .. } => {
for v in vars {
let initial = default_for(v.ty.as_ref());
frame.locals.insert(v.name.to_ascii_lowercase(), initial);
}
Ok(Flow::Normal)
}
Stmt::Const { vars, .. } => {
for v in vars {
let value = match &v.value {
Some(e) => self.eval(e, frame)?,
None => Variant::Empty,
};
frame.locals.insert(v.name.to_ascii_lowercase(), value);
}
Ok(Flow::Normal)
}
Stmt::Assign {
target, value, set, ..
} => {
let v = self.eval(value, frame)?;
let v = if *set { v } else { self.scalar(v)? };
self.assign_with(target, v, frame, module_level, *set)?;
Ok(Flow::Normal)
}
Stmt::Call { expr, .. } => {
self.eval(expr, frame)?;
Ok(Flow::Normal)
}
Stmt::If {
branches,
else_body,
..
} => {
for (cond, body) in branches {
let c = self.eval(cond, frame)?;
if !c.is_null() && c.to_bool()? {
return self.exec_block(body, frame);
}
}
match else_body {
Some(body) => self.exec_block(body, frame),
None => Ok(Flow::Normal),
}
}
Stmt::SelectCase {
subject,
cases,
case_else,
..
} => {
let subject_is_const_text = is_constant(subject);
let subject_is_static_bool = is_statically_boolean(subject);
let subject = self.eval(subject, frame)?;
let text_compare = subject_is_const_text && matches!(subject, Variant::Str(_));
let bool_compare = subject_is_static_bool && matches!(subject, Variant::Boolean(_));
for case in cases {
for m in &case.matches {
if self.case_matches(&subject, m, frame, text_compare, bool_compare)? {
return self.exec_block(&case.body, frame);
}
}
}
match case_else {
Some(body) => self.exec_block(body, frame),
None => Ok(Flow::Normal),
}
}
Stmt::For {
var,
from,
to,
step,
body,
..
} => self.exec_for(var, from, to, step.as_ref(), body, frame),
Stmt::ForEach {
var,
iterable,
body,
..
} => self.exec_for_each(var, iterable, body, frame),
Stmt::DoLoop {
pre, post, body, ..
} => self.exec_do(pre.as_ref(), post.as_ref(), body, frame),
Stmt::With { subject, body, .. } => {
let subject = self.eval(subject, frame)?;
frame.with_stack.push(subject);
let flow = self.exec_block(body, frame);
frame.with_stack.pop();
flow
}
Stmt::Exit { kind, .. } => Ok(match kind {
ExitKind::Sub | ExitKind::Function | ExitKind::Property => Flow::ExitProc,
ExitKind::For => Flow::ExitFor,
ExitKind::Do | ExitKind::While => Flow::ExitDo,
}),
Stmt::GoTo { label, .. } => Ok(Flow::Goto(label.clone())),
Stmt::OnError { kind, .. } => {
frame.handler = match kind {
OnErrorKind::GoTo(label) => Handler::Goto(label.clone()),
OnErrorKind::ResumeNext => Handler::ResumeNext,
OnErrorKind::Disable => Handler::None,
};
frame.in_handler = false;
Ok(Flow::Normal)
}
Stmt::Resume { kind, .. } => {
frame.in_handler = false;
Ok(match kind {
ResumeKind::Label(label) => Flow::Goto(label.clone()),
ResumeKind::Next => Flow::Goto("\0resume-next".to_string()),
ResumeKind::Retry => Flow::Goto("\0resume-next".to_string()),
})
}
Stmt::Stop { .. } | Stmt::End { .. } => Ok(Flow::ExitProc),
Stmt::ReDim { .. } => Err(out_of_scope("ReDim")),
Stmt::Erase { .. } => Err(out_of_scope("Erase")),
Stmt::GoSub { .. } | Stmt::Return { .. } => Err(out_of_scope("GoSub")),
Stmt::OnGoto { .. } => Err(out_of_scope("On ... GoTo")),
Stmt::TypeDef { .. } => Err(out_of_scope("Type")),
Stmt::EnumDef { .. } => Err(out_of_scope("Enum")),
Stmt::Declare { .. } => Err(out_of_scope("Declare")),
Stmt::EventDef { .. } | Stmt::RaiseEvent { .. } => Err(out_of_scope("events")),
Stmt::Implements { .. } => Err(out_of_scope("Implements")),
Stmt::Opaque { keyword, .. } => Err(out_of_scope(keyword)),
}
}
fn case_matches(
&mut self,
subject: &Variant,
m: &CaseMatch,
frame: &mut Frame,
text_compare: bool,
bool_compare: bool,
) -> VResult<bool> {
let cmp =
|lhs: &Variant, rhs: &Variant, kind: Operand| -> VResult<Option<std::cmp::Ordering>> {
if text_compare {
return Ok(Some(lhs.to_vba_string()?.cmp(&rhs.to_vba_string()?)));
}
value::compare_ctx(lhs, rhs, Operand::Runtime, kind)
};
let cast = |v: Variant| -> VResult<Variant> {
if bool_compare {
return Ok(Variant::Boolean(v.to_bool()?));
}
Ok(v)
};
Ok(match m {
CaseMatch::Value(e) => {
let v = cast(self.eval(e, frame)?)?;
cmp(subject, &v, operand_kind(e))? == Some(std::cmp::Ordering::Equal)
}
CaseMatch::Range(lo_e, hi_e) => {
if subject.is_null() {
return Ok(true);
}
let lo = cast(self.eval(lo_e, frame)?)?;
let hi = cast(self.eval(hi_e, frame)?)?;
let a = cmp(subject, &lo, operand_kind(lo_e))?;
let b = cmp(subject, &hi, operand_kind(hi_e))?;
matches!(a, Some(o) if o != std::cmp::Ordering::Less)
&& matches!(b, Some(o) if o != std::cmp::Ordering::Greater)
}
CaseMatch::Is(op, e) => {
let v = cast(self.eval(e, frame)?)?;
let ord = cmp(subject, &v, operand_kind(e))?;
match ord {
None => false,
Some(o) => compare_with(*op, o),
}
}
})
}
fn exec_for(
&mut self,
var: &Expr,
from: &Expr,
to: &Expr,
step: Option<&Expr>,
body: &[Stmt],
frame: &mut Frame,
) -> VResult<Flow> {
let start = self.eval(from, frame)?.to_f64()?;
let limit = self.eval(to, frame)?.to_f64()?;
let step_v = match step {
Some(e) => self.eval(e, frame)?.to_f64()?,
None => 1.0,
};
if step_v == 0.0 {
return Err(VbaError::new(
5,
"Invalid procedure call or argument: For step is 0",
));
}
let mut current = start;
loop {
self.tick()?;
self.assign(var, number_like(current, start, step_v), frame, false)?;
let done = if step_v > 0.0 {
current > limit
} else {
current < limit
};
if done {
break;
}
match self.exec_block(body, frame)? {
Flow::Normal => {}
Flow::ExitFor => break,
other => return Ok(other),
}
current += step_v;
}
Ok(Flow::Normal)
}
fn exec_do(
&mut self,
pre: Option<&(DoTest, Expr)>,
post: Option<&(DoTest, Expr)>,
body: &[Stmt],
frame: &mut Frame,
) -> VResult<Flow> {
loop {
self.tick()?;
if let Some((test, cond)) = pre {
let c = self.eval(cond, frame)?.to_bool()?;
let go = match test {
DoTest::While => c,
DoTest::Until => !c,
};
if !go {
break;
}
}
match self.exec_block(body, frame)? {
Flow::Normal => {}
Flow::ExitDo => break,
other => return Ok(other),
}
if let Some((test, cond)) = post {
let c = self.eval(cond, frame)?.to_bool()?;
let go = match test {
DoTest::While => c,
DoTest::Until => !c,
};
if !go {
break;
}
}
}
Ok(Flow::Normal)
}
fn assign(
&mut self,
target: &Expr,
v: Variant,
frame: &mut Frame,
module_level: bool,
) -> VResult<()> {
self.assign_with(target, v, frame, module_level, false)
}
fn assign_with(
&mut self,
target: &Expr,
v: Variant,
frame: &mut Frame,
module_level: bool,
set: bool,
) -> VResult<()> {
match target {
Expr::Member {
target: obj, name, ..
} => {
let obj = self.member_owner(obj.as_deref(), frame)?;
let Variant::Object(obj) = obj else {
return Err(VbaError::new(
424,
format!("Object required: .{name} on a {}", obj.type_name()),
));
};
return self
.host(&format!(".{name}"))?
.set_member(&obj, name, &[], &v);
}
Expr::Call {
target: t, args, ..
} if !set => {
if let Expr::Member { .. } | Expr::Ident { .. } = t.as_ref() {
let obj = self.eval(target, frame);
if let Ok(Variant::Object(obj)) = obj {
return self
.host("assignment to an object")?
.assign_default(&obj, &v);
}
let _ = args;
}
return Err(out_of_scope("array or property assignment"));
}
_ => {}
}
match target {
Expr::Ident { name, .. } => {
let key = name.to_ascii_lowercase();
let writes_global = !module_level
&& !frame.locals.contains_key(&key)
&& self.globals.contains_key(&key);
if writes_global {
self.globals.insert(key, v);
} else {
frame.locals.insert(key, v);
}
Ok(())
}
Expr::Bang { .. } => Err(out_of_scope("property assignment")),
Expr::Member { .. } | Expr::Call { .. } => {
Err(out_of_scope("array or property assignment"))
}
other => Err(VbaError::new(
erl_assign_error(),
format!("Cannot assign to this expression ({other:?})"),
)),
}
}
fn lookup(&self, name: &str, frame: &Frame) -> Option<Variant> {
let key = name.to_ascii_lowercase();
frame
.locals
.get(&key)
.or_else(|| self.globals.get(&key))
.cloned()
}
fn eval(&mut self, e: &Expr, frame: &mut Frame) -> VResult<Variant> {
self.tick()?;
match e {
Expr::Literal(l) => Ok(literal_to_variant(l)),
Expr::Paren { expr, .. } => self.eval(expr, frame),
Expr::Ident { name, .. } => {
if let Some(v) = self.lookup(name, frame) {
return Ok(v);
}
if let Some(v) = self.builtin_constant(name) {
return Ok(v);
}
if let Some(h) = self.host.as_mut()
&& let Some(obj) = h.global(name)
{
return Ok(Variant::Object(obj));
}
if self.host.is_none() && super::host::is_host_name(name) {
return Err(needs_workbook(name));
}
if self.find_procedure(name).is_some() {
return self.call_procedure(name, Vec::new());
}
if let Some(v) = builtins::call(name, &[])? {
return Ok(v);
}
Ok(Variant::Empty)
}
Expr::Unary { op, expr, .. } => {
let v = self.eval(expr, frame)?;
let v = self.scalar(v)?;
let mode = if is_constant(expr) {
ArithMode::Constant
} else {
ArithMode::Promote
};
match op {
UnOp::Neg => value::neg(&v, mode),
UnOp::Pos => value::pos(&v, mode),
UnOp::Not => value::not(&v),
}
}
Expr::Binary { op, lhs, rhs, .. } => {
let a = self.eval(lhs, frame)?;
let b = self.eval(rhs, frame)?;
if *op == BinOp::Is {
return is_comparison(&a, &b);
}
let a = self.scalar(a)?;
let b = self.scalar(b)?;
let mode = if is_constant(lhs) && is_constant(rhs) {
ArithMode::Constant
} else {
ArithMode::Promote
};
let kinds = (operand_kind(lhs), operand_kind(rhs));
eval_binary(*op, &a, &b, mode, kinds)
}
Expr::Call { target, args, .. } => self.eval_call(target, args, frame),
Expr::Member { target, name, .. } => {
if let Some(t) = target
&& let Expr::Ident { name: obj, .. } = t.as_ref()
&& obj.eq_ignore_ascii_case("err")
{
return Ok(match name.to_ascii_lowercase().as_str() {
"number" => Variant::Long(self.err.number),
"description" => Variant::Str(self.err.description.clone()),
other => return Err(out_of_scope(&format!("Err.{other}"))),
});
}
self.member(target.as_deref(), name, &[], frame)
}
Expr::Bang { name, .. } => Err(out_of_scope(&format!("!{name}"))),
Expr::Me { .. } => Err(out_of_scope("Me")),
Expr::New { .. } => Err(out_of_scope("New")),
Expr::TypeOf { .. } => Err(out_of_scope("TypeOf")),
Expr::AddressOf { .. } => Err(out_of_scope("AddressOf")),
}
}
fn member_owner(&mut self, target: Option<&Expr>, frame: &mut Frame) -> VResult<Variant> {
match target {
Some(e) => self.eval(e, frame),
None => frame
.with_stack
.last()
.cloned()
.ok_or_else(|| {
VbaError::new(
91,
"Object variable or With block variable not set: a leading '.' outside a With block",
)
}),
}
}
fn member(
&mut self,
target: Option<&Expr>,
name: &str,
args: &[Variant],
frame: &mut Frame,
) -> VResult<Variant> {
let owner = self.member_owner(target, frame)?;
let Variant::Object(obj) = owner else {
return Err(VbaError::new(
424,
format!("Object required: .{name} on a {}", owner.type_name()),
));
};
self.host(&format!(".{name}"))?.get_member(&obj, name, args)
}
fn scalar(&mut self, v: Variant) -> VResult<Variant> {
let Variant::Object(obj) = v else {
return Ok(v);
};
self.host("using an object as a value")?.default_value(&obj)
}
fn exec_for_each(
&mut self,
var: &Expr,
iterable: &Expr,
body: &[Stmt],
frame: &mut Frame,
) -> VResult<Flow> {
let subject = self.eval(iterable, frame)?;
let items = match subject {
Variant::Object(obj) => self.host("For Each")?.iterate(&obj)?,
Variant::Array(a) => a.values.clone(),
other => {
return Err(VbaError::new(
438,
format!(
"Object doesn't support this property or method: For Each over a {}",
other.type_name()
),
));
}
};
for item in items {
self.tick()?;
self.assign_with(var, item, frame, false, true)?;
match self.exec_block(body, frame)? {
Flow::Normal => {}
Flow::ExitFor => break,
other => return Ok(other),
}
}
Ok(Flow::Normal)
}
fn eval_call(&mut self, target: &Expr, args: &[Arg], frame: &mut Frame) -> VResult<Variant> {
if let Expr::Member {
target: Some(obj),
name,
..
} = target
&& let Expr::Ident { name: o, .. } = obj.as_ref()
&& o.eq_ignore_ascii_case("err")
&& name.eq_ignore_ascii_case("raise")
{
let values = self.eval_args(args, frame)?;
let number = values
.first()
.map(|v| v.to_f64())
.transpose()?
.unwrap_or(0.0) as i32;
let description = match values.get(2) {
Some(v) => v.to_vba_string()?,
None => describe_error(number),
};
return Err(VbaError::new(number, description));
}
if let Expr::Member {
target: obj, name, ..
} = target
{
let values = self.eval_args(args, frame)?;
return self.member(obj.as_deref(), name, &values, frame);
}
let Expr::Ident { name, .. } = target else {
return Err(out_of_scope("this call target"));
};
if self.find_procedure(name).is_some() {
let values = self.eval_args(args, frame)?;
return self.call_procedure(name, values);
}
if let Some(Variant::Array(a)) = self.lookup(name, frame) {
let values = self.eval_args(args, frame)?;
let row = values
.first()
.map(|v| v.to_f64())
.transpose()?
.unwrap_or(0.0);
let col = match values.get(1) {
Some(v) => v.to_f64()?,
None => 0.0,
};
return a.get(row as usize, col as usize);
}
let values = self.eval_args(args, frame)?;
let values = if OBJECT_AWARE_BUILTINS.contains(&name.to_ascii_lowercase().as_str()) {
values
} else {
values
.into_iter()
.map(|v| self.scalar(v))
.collect::<VResult<Vec<_>>>()?
};
if let Some(v) = builtins::call(name, &values)? {
return Ok(v);
}
if let Some(h) = self.host.as_mut()
&& let Some(r) = h.global_call(name, &values)
{
return r;
}
if self.host.is_none() && super::host::is_host_name(name) {
return Err(needs_workbook(name));
}
Err(VbaError::new(
35,
format!("Sub or Function not defined: {name}"),
))
}
fn eval_args(&mut self, args: &[Arg], frame: &mut Frame) -> VResult<Vec<Variant>> {
let mut out = Vec::with_capacity(args.len());
for a in args {
match &a.value {
Some(e) => out.push(self.eval(e, frame)?),
None => out.push(Variant::Empty),
}
}
Ok(out)
}
fn builtin_constant(&self, name: &str) -> Option<Variant> {
Some(match name.to_ascii_lowercase().as_str() {
"vbnullstring" => Variant::Str(String::new()),
"vbcrlf" => Variant::Str("\r\n".to_string()),
"vbcr" => Variant::Str("\r".to_string()),
"vblf" => Variant::Str("\n".to_string()),
"vbtab" => Variant::Str("\t".to_string()),
"vbnewline" => Variant::Str("\n".to_string()),
"vbobjecterror" => Variant::Long(-2147221504),
_ => return None,
})
}
}
fn erl_label_error() -> i32 {
13
}
fn erl_assign_error() -> i32 {
13
}
fn describe_error(number: i32) -> String {
match number {
5 => "Invalid procedure call or argument",
6 => "Overflow",
9 => "Subscript out of range",
11 => "Division by zero",
13 => "Type mismatch",
94 => "Invalid use of Null",
_ => "Application-defined or object-defined error",
}
.to_string()
}
fn compare_with(op: BinOp, ord: std::cmp::Ordering) -> bool {
use std::cmp::Ordering::*;
match op {
BinOp::Eq => ord == Equal,
BinOp::Ne => ord != Equal,
BinOp::Lt => ord == Less,
BinOp::Gt => ord == Greater,
BinOp::Le => ord != Greater,
BinOp::Ge => ord != Less,
_ => false,
}
}
fn is_constant(e: &Expr) -> bool {
match e {
Expr::Literal(Literal::Null) => false,
Expr::Literal(_) => true,
Expr::Paren { expr, .. } => is_constant(expr),
Expr::Unary { expr, .. } => is_constant(expr),
Expr::Binary { lhs, rhs, .. } => is_constant(lhs) && is_constant(rhs),
_ => false,
}
}
const STATICALLY_NUMERIC: &[&str] = &[
"cint", "clng", "cdbl", "csng", "ccur", "cbool", "cbyte", "len", "val", "sgn",
];
const STATICALLY_BOOLEAN: &[&str] = &[
"cbool",
"isnumeric",
"isnull",
"isempty",
"isdate",
"isobject",
"isarray",
"iserror",
];
const STATICALLY_STRING: &[&str] = &["cstr", "typename"];
fn is_statically_boolean(e: &Expr) -> bool {
match e {
Expr::Paren { expr, .. } => is_statically_boolean(expr),
Expr::Unary {
op: UnOp::Not,
expr,
..
} => is_statically_boolean(expr),
Expr::Call { target, .. } => matches!(target.as_ref(), Expr::Ident { name, .. }
if STATICALLY_BOOLEAN.contains(&name.to_ascii_lowercase().as_str())),
_ => is_constant(e),
}
}
fn is_statically_typed(e: &Expr) -> bool {
match e {
Expr::Literal(_) => true,
Expr::Paren { expr, .. } | Expr::Unary { expr, .. } => is_statically_typed(expr),
Expr::Binary { op, lhs, rhs, .. } => {
matches!(
op,
BinOp::Add
| BinOp::Sub
| BinOp::Mul
| BinOp::Div
| BinOp::IntDiv
| BinOp::Mod
| BinOp::Pow
) && is_statically_typed(lhs)
&& is_statically_typed(rhs)
}
Expr::Call { target, .. } => matches!(target.as_ref(), Expr::Ident { name, .. }
if {
let name = name.to_ascii_lowercase();
STATICALLY_NUMERIC.contains(&name.as_str())
|| STATICALLY_BOOLEAN.contains(&name.as_str())
|| STATICALLY_STRING.contains(&name.as_str())
}),
_ => false,
}
}
fn operand_kind(e: &Expr) -> Operand {
let statically_typed = is_statically_typed(e);
match e {
Expr::Literal(_) => Operand::Literal,
Expr::Paren { expr, .. } | Expr::Unary { expr, .. }
if operand_kind(expr) == Operand::Literal =>
{
Operand::Literal
}
_ if is_constant(e) => Operand::ConstExpr,
_ if statically_typed => Operand::Static,
_ => Operand::Runtime,
}
}
fn constant_bool_int_op(op: BinOp, a: &Variant, b: &Variant, mode: ArithMode) -> Option<()> {
(mode == ArithMode::Constant
&& matches!(op, BinOp::IntDiv | BinOp::Mod)
&& matches!(a, Variant::Boolean(_))
&& matches!(b, Variant::Str(_)))
.then_some(())
}
fn eval_binary(
op: BinOp,
a: &Variant,
b: &Variant,
mode: ArithMode,
kinds: (Operand, Operand),
) -> VResult<Variant> {
use BinOp::*;
if constant_bool_int_op(op, a, b, mode).is_some() {
let l: i64 = if a.to_bool()? { -1 } else { 0 };
let r: i64 = if b.to_bool()? { -1 } else { 0 };
if r == 0 {
return Err(VbaError::div_by_zero());
}
let v = if op == IntDiv { l / r } else { l % r };
return Ok(Variant::Boolean(v != 0));
}
match op {
Add => value::add(a, b, mode),
Sub => value::sub(a, b, mode),
Mul => value::mul(a, b, mode),
Div => value::div(a, b),
IntDiv => value::int_div(a, b),
Mod => value::modulo(a, b),
Pow => value::pow(a, b, mode),
Concat => value::concat(a, b),
Eq | Ne | Lt | Gt | Le | Ge => match value::compare_ctx(a, b, kinds.0, kinds.1)? {
None => Ok(Variant::Null),
Some(ord) => Ok(Variant::Boolean(compare_with(op, ord))),
},
And => null_on_the_right(a, b, kinds, value::and(a, b, kinds)),
Or => null_on_the_right(a, b, kinds, value::or(a, b, kinds)),
Xor => null_on_the_right(a, b, kinds, value::logical(a, b, kinds, |x, y| x ^ y)),
Eqv => null_on_the_right(a, b, kinds, value::logical(a, b, kinds, |x, y| !(x ^ y))),
Imp => null_on_the_right(a, b, kinds, value::imp(a, b, kinds)),
Like => Err(out_of_scope("Like")),
Is => is_comparison(a, b),
}
}
const OBJECT_AWARE_BUILTINS: &[&str] = &["typename", "vartype", "isobject"];
fn is_comparison(a: &Variant, b: &Variant) -> VResult<Variant> {
match (a.as_object(), b.as_object()) {
(Some(x), Some(y)) => Ok(Variant::Boolean(x.same_object(y))),
_ => Err(VbaError::new(424, "Object required: Is compares objects")),
}
}
fn null_on_the_right(
lhs: &Variant,
rhs: &Variant,
kinds: (Operand, Operand),
computed: VResult<Variant>,
) -> VResult<Variant> {
let statically_string = matches!(lhs, Variant::Str(_)) && kinds.0 != Operand::Runtime;
if statically_string && rhs.is_null() {
computed?;
return Err(VbaError::invalid_null());
}
computed
}
fn literal_to_variant(l: &Literal) -> Variant {
use super::lexer::TypeSuffix;
match l {
Literal::Number {
value,
base,
suffix,
is_float,
} => match suffix {
Some(TypeSuffix::Integer) => Variant::Integer(*value as i16),
Some(TypeSuffix::Long) => Variant::Long(*value as i32),
Some(TypeSuffix::Single) => Variant::Single(*value as f32),
Some(TypeSuffix::Double) => Variant::Double(*value),
Some(TypeSuffix::Currency) => Variant::Currency((value * 10_000.0).round() as i64),
Some(TypeSuffix::String) => Variant::Str(value::format_number(*value)),
None => {
let _ = base;
Variant::from_literal(*value, *is_float || value.fract() != 0.0)
}
},
Literal::Str(s) => Variant::Str(s.clone()),
Literal::Bool(b) => Variant::Boolean(*b),
Literal::Empty => Variant::Empty,
Literal::Null => Variant::Null,
Literal::Date(text) => match crate::core::date::parse_date(text) {
Some((d, _)) => Variant::Date(crate::core::date::date_to_excel_serial(d)),
None => Variant::Empty,
},
Literal::Nothing => Variant::Object(ObjRef::Nothing),
}
}
fn number_like(current: f64, start: f64, step: f64) -> Variant {
let integral = current.fract() == 0.0 && start.fract() == 0.0 && step.fract() == 0.0;
Variant::from_literal(current, !integral)
}
fn default_for(ty: Option<&TypeRef>) -> Variant {
let Some(ty) = ty else {
return Variant::Empty;
};
let Some(last) = ty.path.last() else {
return Variant::Empty;
};
match last.to_ascii_lowercase().as_str() {
"integer" => Variant::Integer(0),
"long" => Variant::Long(0),
"single" => Variant::Single(0.0),
"double" => Variant::Double(0.0),
"currency" => Variant::Currency(0),
"boolean" => Variant::Boolean(false),
"string" => Variant::Str(String::new()),
"date" => Variant::Date(0.0),
"range" | "worksheet" | "workbook" | "object" | "application" | "sheets" => {
Variant::Object(ObjRef::Nothing)
}
_ => Variant::Empty,
}
}
#[cfg(test)]
mod tests {
use super::super::parser::parse_module;
use super::*;
fn run(body: &str) -> String {
let src = format!("Function F()\n{body}\nEnd Function\n");
let module = parse_module(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
match Interpreter::new(module).run("F", Vec::new()) {
Ok(v) => format!(
"{}|{}",
v.type_name(),
v.to_vba_string().unwrap_or_default()
),
Err(e) => format!("ERR|{}", e.number),
}
}
fn expr(e: &str) -> String {
run(&format!(" F = {e}"))
}
#[test]
fn arithmetic_and_types_match_the_excel_probe() {
assert_eq!(expr("1 + 1"), "Integer|2");
assert_eq!(expr("32767 + 1"), "ERR|6");
assert_eq!(expr("1 / 2"), "Double|0.5");
assert_eq!(expr("4 / 2"), "Double|2");
assert_eq!(expr("7 \\ 2"), "Integer|3");
assert_eq!(expr("-7 \\ 2"), "Integer|-3");
assert_eq!(expr("7.6 \\ 2"), "Long|4");
assert_eq!(expr("7 Mod 2"), "Integer|1");
assert_eq!(expr("-7 Mod 2"), "Integer|-1");
assert_eq!(expr("7.6 Mod 2"), "Long|0");
assert_eq!(expr("2 ^ 2"), "Double|4");
assert_eq!(expr("1.5 + 1"), "Double|2.5");
assert_eq!(expr("1 / 0"), "ERR|11");
}
#[test]
fn precedence_is_the_one_measured_in_phase_0() {
assert_eq!(expr("2 ^ 3 ^ 2"), "Double|64");
assert_eq!(expr("-2 ^ 2"), "Double|-4");
assert_eq!(expr("2 + 3 & 4"), "String|54");
assert_eq!(expr("1 = 1 And 1 = 0"), "Boolean|False");
assert_eq!(expr("Not 1 = 0"), "Boolean|True");
assert_eq!(expr("2 * 10 \\ 3"), "Integer|6");
assert_eq!(expr("1 + 7 Mod 3"), "Integer|2");
}
#[test]
fn string_coercion_matches_the_probe() {
assert_eq!(expr("\"1\" + 1"), "Double|2");
assert_eq!(expr("\"1\" + \"2\""), "String|12");
assert_eq!(expr("\"abc\" + 1"), "ERR|13");
assert_eq!(expr("1 & 2"), "String|12");
assert_eq!(expr("\" 3 \" + 1"), "Double|4");
}
#[test]
fn booleans_and_bitwise_operators_match_the_probe() {
assert_eq!(expr("True + 1"), "Integer|0");
assert_eq!(expr("True + True"), "Integer|-2");
assert_eq!(expr("True And False"), "Boolean|False");
assert_eq!(expr("5 And 3"), "Integer|1");
assert_eq!(expr("Not 5"), "Integer|-6");
assert_eq!(expr("CInt(True)"), "Integer|-1");
}
#[test]
fn empty_and_null_behave_as_measured() {
assert_eq!(expr("Empty + 1"), "Integer|1");
assert_eq!(expr("Empty & \"a\""), "String|a");
assert_eq!(expr("Null & \"a\""), "String|a");
assert_eq!(expr("IsNull(Null + 1)"), "Boolean|True");
assert_eq!(expr("Empty = 0"), "Boolean|True");
assert_eq!(expr("Empty = \"\""), "Boolean|True");
}
#[test]
fn conversions_use_bankers_rounding() {
assert_eq!(expr("CLng(0.5)"), "Long|0");
assert_eq!(expr("CLng(1.5)"), "Long|2");
assert_eq!(expr("CLng(2.5)"), "Long|2");
assert_eq!(expr("CLng(-1.5)"), "Long|-2");
assert_eq!(expr("CInt(32768)"), "ERR|6");
assert_eq!(expr("Int(-1.5)"), "Double|-2");
assert_eq!(expr("Fix(-1.5)"), "Double|-1");
assert_eq!(expr("CDbl(\"1e3\")"), "Double|1000");
}
#[test]
fn for_loops_run_and_can_be_exited() {
assert_eq!(
run(" Dim t\n For i = 1 To 5\n t = t + i * i\n Next i\n F = t"),
"Integer|55"
);
assert_eq!(
run(
" Dim t\n For i = 1 To 10\n If i > 3 Then Exit For\n t = t + 1\n Next i\n F = t"
),
"Integer|3"
);
assert_eq!(
run(" Dim t\n For i = 5 To 1 Step -1\n t = t + i\n Next i\n F = t"),
"Integer|15"
);
assert_eq!(
run(
" Dim t\n t = 0\n For i = 5 To 1\n t = t + 1\n Next i\n F = t"
),
"Integer|0"
);
}
#[test]
fn every_do_form_terminates_correctly() {
assert_eq!(
run(" Dim i\n i = 0\n Do While i < 5\n i = i + 1\n Loop\n F = i"),
"Integer|5"
);
assert_eq!(
run(
" Dim i\n i = 0\n Do Until i >= 5\n i = i + 1\n Loop\n F = i"
),
"Integer|5"
);
assert_eq!(
run(" Dim i\n i = 9\n Do\n i = i + 1\n Loop While i < 5\n F = i"),
"Integer|10"
);
assert_eq!(
run(" Dim i\n i = 0\n While i < 3\n i = i + 1\n Wend\n F = i"),
"Integer|3"
);
}
#[test]
fn select_case_covers_values_ranges_and_is() {
let body = |x: &str| {
format!(
" Dim r\n Select Case {x}\n Case 1, 2\n r = \"a\"\n \
Case 3 To 5\n r = \"b\"\n Case Is >= 6\n r = \"c\"\n \
Case Else\n r = \"d\"\n End Select\n F = r"
)
};
assert_eq!(run(&body("2")), "String|a");
assert_eq!(run(&body("4")), "String|b");
assert_eq!(run(&body("9")), "String|c");
assert_eq!(run(&body("0")), "String|d");
}
#[test]
fn if_elseif_else_picks_one_branch() {
let body = |x: &str| {
format!(
" Dim r\n If {x} > 5 Then\n r = 1\n ElseIf {x} > 2 Then\n \
r = 2\n Else\n r = 3\n End If\n F = r"
)
};
assert_eq!(run(&body("9")), "Integer|1");
assert_eq!(run(&body("4")), "Integer|2");
assert_eq!(run(&body("1")), "Integer|3");
}
#[test]
fn goto_jumps_to_a_procedure_level_label() {
assert_eq!(
run(" Dim t\n t = 1\n GoTo Skip\n t = 99\nSkip:\n F = t"),
"Integer|1"
);
}
#[test]
fn functions_call_each_other_and_return_by_name() {
let src = "Function Outer()\n Outer = Inner(3) + Inner(4)\nEnd Function\n\
Function Inner(n)\n Inner = n * n\nEnd Function\n";
let m = parse_module(src).unwrap();
let v = Interpreter::new(m).run("Outer", Vec::new()).unwrap();
assert_eq!(v, Variant::Integer(25));
}
#[test]
fn recursion_works_and_is_bounded() {
let src = "Function Fact(n)\n If n <= 1 Then\n Fact = 1\n Else\n \
Fact = n * Fact(n - 1)\n End If\nEnd Function\n";
let m = parse_module(src).unwrap();
let v = Interpreter::new(m)
.run("Fact", vec![Variant::Integer(5)])
.unwrap();
assert_eq!(v, Variant::Integer(120));
let src = "Function Boom()\n Boom = Boom()\nEnd Function\n";
let m = parse_module(src).unwrap();
let e = Interpreter::new(m).run("Boom", Vec::new()).unwrap_err();
assert_eq!(e.number, 28);
}
#[test]
fn a_sub_returns_empty_and_exits_early() {
let src = "Sub S()\n Exit Sub\nEnd Sub\n";
let m = parse_module(src).unwrap();
assert_eq!(
Interpreter::new(m).run("S", Vec::new()).unwrap(),
Variant::Empty
);
}
#[test]
fn an_infinite_loop_hits_the_op_budget_instead_of_hanging() {
let src = "Function F()\n Do While True\n Loop\nEnd Function\n";
let m = parse_module(src).unwrap();
let e = Interpreter::new(m)
.with_max_ops(10_000)
.run("F", Vec::new())
.unwrap_err();
assert_eq!(e.number, 16);
}
#[test]
fn on_error_goto_runs_the_handler_and_exposes_err() {
assert_eq!(
run(
" On Error GoTo Failed\n F = 1 / 0\n Exit Function\nFailed:\n \
F = \"ERR|\" & Err.Number"
),
"String|ERR|11"
);
assert_eq!(
run(
" On Error GoTo Failed\n F = CLng(\"nope\")\n Exit Function\nFailed:\n \
F = Err.Description"
),
"String|Type mismatch"
);
}
#[test]
fn on_error_resume_next_continues_at_the_failing_statement() {
assert_eq!(
run(" Dim t\n On Error Resume Next\n t = 1 / 0\n t = 7\n F = t"),
"Integer|7"
);
}
#[test]
fn resume_next_resumes_inside_a_nested_block() {
assert_eq!(
run(
" Dim t\n t = 0\n On Error Resume Next\n For i = 1 To 3\n \
t = t + 1 / 0\n t = t + 1\n Next i\n F = t"
),
"Integer|3"
);
}
#[test]
fn on_error_goto_0_disarms_the_handler() {
let src = "Function F()\n On Error Resume Next\n On Error GoTo 0\n \
F = 1 / 0\nEnd Function\n";
let m = parse_module(src).unwrap();
assert_eq!(
Interpreter::new(m).run("F", Vec::new()).unwrap_err().number,
11
);
}
#[test]
fn an_error_inside_a_handler_is_not_caught_by_the_same_handler() {
let src = "Function F()\n On Error GoTo Failed\n F = 1 / 0\n Exit Function\n\
Failed:\n F = 1 / 0\nEnd Function\n";
let m = parse_module(src).unwrap();
assert_eq!(
Interpreter::new(m).run("F", Vec::new()).unwrap_err().number,
11
);
}
#[test]
fn err_raise_produces_a_catchable_error() {
assert_eq!(
run(
" On Error GoTo Failed\n Err.Raise 5\n Exit Function\nFailed:\n \
F = Err.Number"
),
"Long|5"
);
}
#[test]
fn string_builtins_are_one_based_like_vba() {
assert_eq!(expr("Len(\"abcd\")"), "Long|4");
assert_eq!(expr("Left(\"abcd\", 2)"), "String|ab");
assert_eq!(expr("Right(\"abcd\", 2)"), "String|cd");
assert_eq!(expr("Mid(\"abcd\", 2, 2)"), "String|bc");
assert_eq!(expr("Mid(\"abcd\", 3)"), "String|cd");
assert_eq!(expr("InStr(\"abcd\", \"cd\")"), "Long|3");
assert_eq!(expr("InStr(\"abcd\", \"z\")"), "Long|0");
assert_eq!(expr("InStr(3, \"abcabc\", \"a\")"), "Long|4");
assert_eq!(expr("UCase(\"aB\")"), "String|AB");
assert_eq!(expr("Trim(\" a \")"), "String|a");
assert_eq!(expr("Replace(\"aXbXc\", \"X\", \"-\")"), "String|a-b-c");
assert_eq!(expr("Chr(65)"), "String|A");
assert_eq!(expr("Asc(\"A\")"), "Integer|65");
assert_eq!(expr("Mid(\"abcd\", 0)"), "ERR|5");
}
#[test]
fn inspection_builtins_report_the_subtype() {
assert_eq!(expr("TypeName(1)"), "String|Integer");
assert_eq!(expr("TypeName(1.5)"), "String|Double");
assert_eq!(expr("TypeName(\"a\")"), "String|String");
assert_eq!(expr("TypeName(True)"), "String|Boolean");
assert_eq!(expr("TypeName(100000)"), "String|Long");
assert_eq!(expr("IsNumeric(\"12\")"), "Boolean|True");
assert_eq!(expr("IsNumeric(\"ab\")"), "Boolean|False");
assert_eq!(expr("IsEmpty(Empty)"), "Boolean|True");
}
#[test]
fn math_builtins_keep_the_arguments_width() {
assert_eq!(expr("Abs(-3)"), "Integer|3");
assert_eq!(expr("Abs(-3.5)"), "Double|3.5");
assert_eq!(expr("Sgn(-9)"), "Integer|-1");
assert_eq!(expr("Sqr(9)"), "Double|3");
assert_eq!(expr("Sqr(-1)"), "ERR|5");
}
#[test]
fn a_typed_dim_starts_at_its_types_zero_not_empty() {
assert_eq!(
run(" Dim s As String\n F = TypeName(s)"),
"String|String"
);
assert_eq!(run(" Dim n As Long\n F = TypeName(n)"), "String|Long");
assert_eq!(run(" Dim v\n F = TypeName(v)"), "String|Empty");
}
#[test]
fn and_or_and_imp_are_three_valued() {
assert_eq!(expr("False And Null"), "Boolean|False");
assert_eq!(expr("True Or Null"), "Boolean|True");
assert_eq!(expr("IsNull(True And Null)"), "Boolean|True");
assert_eq!(expr("IsNull(False Or Null)"), "Boolean|True");
assert_eq!(
run(" Dim a\n a = 0\n F = (a And Null)"),
"Integer|0"
);
assert_eq!(
run(" Dim a\n a = 5\n F = (a Or Null)"),
"Integer|5"
);
assert_eq!(
run(" Dim a\n a = 5\n F = IsNull(a And Null)"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = 0\n F = IsNull(a Or Null)"),
"Boolean|True"
);
assert_eq!(expr("Null Imp True"), "Boolean|True");
assert_eq!(expr("False Imp Null"), "Boolean|True");
assert_eq!(expr("IsNull(Null Xor True)"), "Boolean|True");
assert_eq!(expr("IsNull(Null Eqv True)"), "Boolean|True");
assert_eq!(expr("IsNull(Not Null)"), "Boolean|True");
}
#[test]
fn string_versus_number_comparison_depends_on_constant_ness() {
assert_eq!(expr("\"10\" = 10"), "Boolean|True");
assert_eq!(expr("\"2\" > 10"), "Boolean|False");
assert_eq!(expr("\"\" = 0"), "ERR|13");
assert_eq!(expr("\"abc\" > 1"), "ERR|13");
assert_eq!(
run(" Dim a\n a = \"2\"\n F = (a > 10)"),
"Boolean|False"
);
assert_eq!(
run(" Dim a\n a = \"1.5\"\n F = (a = 1.5)"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = \"\"\n F = (a = 0)"),
"Boolean|False"
);
assert_eq!(
run(" Dim a\n a = \"abc\"\n F = (a = 1)"),
"Boolean|False"
);
assert_eq!(
run(" Dim b\n b = 10\n F = (\"2\" > b)"),
"Boolean|True"
);
assert_eq!(
run(" Dim b\n b = 1\n F = (\"abc\" > b)"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = True\n F = ((1.5 & \"abc\") <> CLng(a))"),
"ERR|13"
);
assert_eq!(
run(" Dim a\n a = -1\n F = ((1.5 & \"abc\") <> a)"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = 2147483647\n F = (\"Z\" <> a)"),
"Boolean|True"
);
assert_eq!(expr("(Not 2!) <= (\"1.5\" & False)"), "Boolean|True");
assert_eq!(expr("(-True) <> (True & &HFF)"), "ERR|13");
assert_eq!(expr("\"False\" = -0.04"), "ERR|13");
assert_eq!(expr("\"1.5abc\" > 1"), "Boolean|True");
assert_eq!(expr("(False & Null) = (0.1 / -2.5)"), "Boolean|False");
assert_eq!(
run(" Dim a\n a = True\n F = ((1.5 & \"abc\") <> CLng(a))"),
"ERR|13"
);
assert_eq!(
run(" Dim a\n a = 1\n F = ((\"abc\" & a) <> Len(CStr(\"Z\")))"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = \"True\"\n F = (a = True)"),
"Boolean|True"
);
assert_eq!(expr("\"True\" = -1"), "ERR|13");
assert_eq!(
run(" Dim a, b\n a = \"1.5\"\n b = 1.5\n F = (a = b)"),
"Boolean|False"
);
assert_eq!(
run(" Dim a, b\n a = \"2\"\n b = 10\n F = (a > b)"),
"Boolean|True"
);
}
#[test]
fn a_constant_string_select_subject_compares_as_text() {
let sel = |subject: &str| {
format!(
" Dim r\n Select Case {subject}\n Case 2 To 5\n r = \"range\"\n \
Case Else\n r = \"else\"\n End Select\n F = r"
)
};
assert_eq!(run(&sel("\"32768abc\"")), "String|range");
assert_eq!(run(&sel("(32768 & \"abc\")")), "String|range");
assert_eq!(run(&sel("\"3\"")), "String|range");
assert_eq!(run(&sel("\"abc\"")), "String|else");
assert_eq!(run(&sel("\"7\"")), "String|else");
assert_eq!(run(&sel("\"1x\"")), "String|else");
assert_eq!(run(&sel("\"\"")), "String|else");
assert_eq!(run(&sel("3")), "String|range");
assert_eq!(run(&sel("7")), "String|else");
let sel_var = |value: &str| {
format!(
" Dim a, r\n a = {value}\n Select Case a\n Case 2 To 5\n \
r = \"range\"\n Case Else\n r = \"else\"\n End Select\n F = r"
)
};
assert_eq!(run(&sel_var("\"32768abc\"")), "String|else");
assert_eq!(run(&sel_var("\"3\"")), "String|range");
assert_eq!(run(&sel_var("\"7\"")), "String|else");
assert_eq!(run(&sel_var("\"abc\"")), "String|else");
}
#[test]
fn a_constant_string_subject_also_governs_value_and_is_cases() {
let sel = |cases: &str| {
format!(
" Dim r\n Select Case \"abc\"\n{cases} Case Else\n r = \"else\"\n End Select\n F = r"
)
};
assert_eq!(
run(&sel(" Case 3\n r = \"value\"\n")),
"String|else"
);
assert_eq!(
run(&sel(" Case Is >= 2\n r = \"is\"\n")),
"String|is"
);
}
#[test]
fn a_case_range_matches_a_null_subject_but_no_other_case_form_does() {
let sel = |cases: &str| {
format!(
" Dim r\n Select Case Null\n{cases} Case Else\n r = \"else\"\n End Select\n F = r"
)
};
assert_eq!(
run(&sel(" Case 2 To 5\n r = \"range\"\n")),
"String|range"
);
assert_eq!(
run(&sel(" Case 0, 1\n r = \"value\"\n")),
"String|else"
);
assert_eq!(
run(&sel(" Case Is > 2\n r = \"is\"\n")),
"String|else"
);
}
#[test]
fn zero_divided_by_zero_is_overflow_not_division_by_zero() {
assert_eq!(expr("1 / 0"), "ERR|11");
assert_eq!(expr("-1 / 0"), "ERR|11");
assert_eq!(expr("1.5 / 0"), "ERR|11");
assert_eq!(expr("0 / 0"), "ERR|6");
assert_eq!(expr("False / 0"), "ERR|6");
assert_eq!(expr("0 \\ 0"), "ERR|11");
assert_eq!(expr("0 Mod 0"), "ERR|11");
}
#[test]
fn division_coerces_both_operands_before_testing_the_divisor() {
assert_eq!(expr("\"xxxx\" / 0"), "ERR|13");
assert_eq!(expr("\"\" / 0"), "ERR|13");
assert_eq!(expr("0 / \"xxxx\""), "ERR|13");
assert_eq!(expr("\"abc\" / Null"), "ERR|13");
}
#[test]
fn a_static_string_over_a_null_is_invalid_use_of_null() {
for e in [
"\" 3 \" Imp Null",
"\"3\" And Null",
"\"1.5\" Or Null",
"\"0\" Or Null",
"\" 3 \" Xor Null",
"\" 3 \" Eqv Null",
"(\" \" & \"3\") Or Null",
"CStr(3) Or Null",
] {
assert_eq!(expr(e), "ERR|94", "{e}");
}
assert_eq!(
run(" Dim a\n a = Null\n F = IsNull(\" 3 \" Or a)"),
"ERR|94"
);
assert_eq!(
run(" Dim a\n a = \" 3 \"\n F = IsNull(a Imp Null)"),
"Boolean|False"
);
assert_eq!(expr("IsNull(Null Or \" 3 \")"), "Boolean|False");
assert_eq!(expr("IsNull(Null Xor \" 3 \")"), "Boolean|True");
assert_eq!(expr("\"abc\" Imp Null"), "ERR|13");
assert_eq!(expr("\"True\" Or Null"), "ERR|13");
assert_eq!(expr("IsNull(255 Imp Null)"), "Boolean|False");
}
#[test]
fn a_statically_typed_numeric_partner_is_strict_only_against_a_constant_string() {
let with = |e: &str| run(&format!(" Dim va\n va = 1\n F = {e}"));
for f in ["CLng(va)", "Len(CStr(va))", "Val(CStr(va))", "Sgn(va)"] {
assert_eq!(with(&format!("({f} > (-32768 & -2.5))")), "ERR|13", "{f}");
}
for f in ["Int(va)", "Abs(va)", "va"] {
assert_eq!(
with(&format!("({f} > (-32768 & -2.5))")),
"Boolean|True",
"{f}"
);
}
assert_eq!(
run(" Dim va, vb\n va = 5\n vb = \"1\"\n F = (CLng(va) < vb)"),
"Boolean|False"
);
assert_eq!(with("(CLng(va) < (\"abc\" & va))"), "Boolean|True");
}
#[test]
fn negating_the_long_minimum_between_constants_wraps_to_itself() {
assert_eq!(expr("TypeName(-(Not 2147483647))"), "String|Long");
assert_eq!(expr("CStr(-(Not 2147483647))"), "String|-2147483648");
assert_eq!(expr("CStr(-(Not 32767))"), "ERR|6");
assert_eq!(
run(" Dim a\n a = 2147483647\n F = CStr(-(Not a))"),
"String|2147483648"
);
}
#[test]
fn select_case_sees_not_of_a_boolean_as_statically_boolean() {
let sel = |subject: &str| {
run(&format!(
" Dim c\n Select Case {subject}\n Case 0, 1\n c = \"one\"\n Case 2 To 5\n c = \"range\"\n Case Else\n c = \"else\"\n End Select\n F = c"
))
};
assert_eq!(sel("(Not IsEmpty(\"Z\"))"), "String|one");
assert_eq!(sel("(Not IsEmpty(\"\"))"), "String|one");
assert_eq!(sel("(Not (IsEmpty(\"Z\")))"), "String|one");
assert_eq!(sel("(Not CBool(0))"), "String|one");
assert_eq!(sel("IsEmpty(\"Z\")"), "String|one");
assert_eq!(sel("(Not 5)"), "String|else");
}
#[test]
fn instr_of_an_empty_haystack_is_zero() {
assert_eq!(expr("CStr(InStr(\"\", \"\"))"), "String|0");
assert_eq!(expr("CStr(InStr(Empty, \"\"))"), "String|0");
assert_eq!(expr("CStr(InStr(\"a\", \"\"))"), "String|1");
assert_eq!(expr("CStr(InStr(\"\", \"a\"))"), "String|0");
}
#[test]
fn static_typing_propagates_through_arithmetic() {
let with = |e: &str| run(&format!(" Dim a\n a = -3\n F = {e}"));
assert_eq!(with("(Len(CStr(a)) = \"-7False\")"), "ERR|13");
assert_eq!(with("((Len(CStr(a)) / 2) = \"-7False\")"), "ERR|13");
assert_eq!(with("((Len(CStr(a)) + 1) = \"-7False\")"), "ERR|13");
assert_eq!(with("((CLng(a) / 2) = \"abc\")"), "ERR|13");
assert_eq!(
with("((Len(CStr(a)) / (-32768)) = ((-7) & (0 > \"1.5\")))"),
"ERR|13"
);
assert_eq!(with("((Len(CStr(a)) + a) = \"-7False\")"), "Boolean|False");
assert_eq!(with("((a / (-32768)) = \"-7False\")"), "Boolean|False");
assert_eq!(with("((a + 1) = \"-7False\")"), "Boolean|False");
assert_eq!(with("((CLng(a) * 2) = \"-6.0\")"), "Boolean|True");
}
#[test]
fn a_string_converts_with_cbool_against_a_static_boolean() {
let with = |setup: &str, e: &str| run(&format!(" Dim va, vb\n{setup}\n F = {e}"));
assert_eq!(with(" va = \"011\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"0\"", "(va = False)"), "Boolean|True");
assert_eq!(with(" va = \"2\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"-1\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"1.5\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"-1\"", "(va <> True)"), "Boolean|False");
assert_eq!(with(" va = \"011\"", "(va < False)"), "Boolean|True");
assert_eq!(with(" va = \"011\"", "(va > False)"), "Boolean|False");
assert_eq!(with(" va = \"011\"", "(va > True)"), "Boolean|False");
assert_eq!(with(" va = \"abc\"", "(va = True)"), "Boolean|False");
assert_eq!(with(" va = \"\"", "(va = False)"), "Boolean|False");
assert_eq!(expr("CStr(32767) >= (Not True)"), "Boolean|False");
assert_eq!(expr("TypeName(32767) >= False"), "ERR|13");
assert_eq!(expr("(TypeName(32767) >= (Not True))"), "ERR|13");
assert_eq!(expr("LCase(\"Integer\") >= (Not True)"), "Boolean|True");
assert_eq!(
run(" Dim va\n va = TypeName(32767)\n F = (va >= (Not True))"),
"Boolean|True"
);
assert_eq!(with(" va = \"011\"", "(va < CBool(0))"), "Boolean|True");
assert_eq!(
with(" va = \"011\"", "(va < IsNull(32768))"),
"Boolean|True"
);
assert_eq!(expr("(\"abc\" < True)"), "ERR|13");
assert_eq!(expr("(\"Z\" < True)"), "ERR|13");
assert_eq!(expr("(False >= \"abc\")"), "ERR|13");
assert_eq!(expr("(\"\" = False)"), "ERR|13");
assert_eq!(expr("(\"011\" < False)"), "Boolean|True");
assert_eq!(expr("(False > \"12\")"), "Boolean|True");
assert_eq!(expr("(\"0\" = False)"), "Boolean|True");
assert_eq!(
expr("((Empty & \"1\") <= (\"\" <> Empty))"),
"Boolean|False"
);
assert_eq!(expr("TypeName(0) >= (3# >= Empty)"), "Boolean|False");
assert_eq!(expr("(3# >= Empty) >= TypeName(0)"), "Boolean|True");
assert_eq!(expr("CStr(0) >= (3# >= Empty)"), "Boolean|False");
assert_eq!(expr("(Not True) <= CStr(32767)"), "Boolean|False");
assert_eq!(expr("False >= TypeName(0)"), "ERR|13");
assert_eq!(expr("(\"000\" < (\"1\" >= -7))"), "Boolean|False");
assert_eq!(
run(" Dim va\n va = \"000\"\n F = (va < (\"1\" >= -7))"),
"Boolean|False"
);
assert_eq!(expr("(Right(100000, 3) < (\"1\" >= -7))"), "Boolean|False");
assert_eq!(expr("(CStr(0) >= CBool(1))"), "Boolean|True");
assert_eq!(expr("(\"000\" < CBool(1))"), "Boolean|False");
assert_eq!(expr("(TypeName(0) >= CBool(1))"), "ERR|13");
assert_eq!(
with(" va = \"011\"\n vb = False", "(va < vb)"),
"Boolean|False"
);
assert_eq!(with(" va = \"True\"", "(va < False)"), "Boolean|True");
assert_eq!(with(" va = \"true\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"TRUE\"", "(va = True)"), "Boolean|True");
assert_eq!(with(" va = \"true\"", "(va = False)"), "Boolean|False");
assert_eq!(
with(" va = \"011\"\n vb = False", "(va < vb)"),
"Boolean|False"
);
assert_eq!(with(" va = \"011\"", "(va < 0)"), "Boolean|False");
assert_eq!(expr("(\"True\" = -1)"), "ERR|13");
}
#[test]
fn division_overflows_rather_than_returning_an_infinity() {
assert_eq!(expr("1E308 / 1E-308"), "ERR|6");
assert_eq!(
run(" Dim a, b\n a = 1E308\n b = 1E-308\n F = a / b"),
"ERR|6"
);
assert_eq!(
run(" Dim a, b\n a = 3.75\n b = a ^ 32767\n F = b / 2"),
"ERR|6"
);
assert_eq!(expr("1 / 2"), "Double|0.5");
assert_eq!(expr("1 / 0"), "ERR|11");
assert_eq!(expr("0 / 0"), "ERR|6");
}
#[test]
fn pow_overflows_between_constants_and_yields_infinity_at_runtime() {
assert_eq!(expr("3.75 ^ 32767"), "ERR|6");
assert_eq!(expr("255 ^ 255"), "ERR|6");
assert_eq!(
run(" Dim a\n a = 3.75\n F = (a ^ 32767)"),
"Double|INF"
);
assert_eq!(
run(" Dim a\n a = 255\n F = (a ^ 255)"),
"Double|INF"
);
assert_eq!(expr("2 ^ 10"), "Double|1024");
}
#[test]
fn infinity_is_a_value_for_pow_but_not_for_arithmetic() {
assert_eq!(
run(" Dim a\n a = 255\n F = (a ^ 255)"),
"Double|INF"
);
assert_eq!(
run(" Dim a\n a = 255\n F = -(a ^ 255)"),
"Double|-INF"
);
assert_eq!(
run(" Dim a\n a = 255\n F = ((a ^ 255) & \"x\")"),
"String|INFx"
);
assert_eq!(
run(" Dim a, b\n a = 255\n b = (a ^ 255)\n F = (b + 1)"),
"ERR|6"
);
assert_eq!(run(" Dim a\n a = 1E300\n F = (a * a)"), "ERR|6");
assert_eq!(
run(" Dim a, b\n a = 1E300\n b = 1E300\n F = (a + b)"),
"Double|2E+300"
);
}
#[test]
fn imp_follows_its_definition_rather_than_a_hand_rolled_table() {
assert_eq!(
run(" Dim a\n a = 255\n F = (a Imp Null)"),
"Integer|-256"
);
assert_eq!(expr("Null Imp True"), "Boolean|True");
assert_eq!(expr("False Imp Null"), "Boolean|True");
assert_eq!(expr("5 Imp 3"), "Integer|-5");
}
#[test]
fn single_combined_with_long_widens_past_both() {
assert_eq!(run(" Dim a\n a = 2!\n F = (a + 1)"), "Single|3");
assert_eq!(
run(" Dim a, b\n a = 2!\n b = 1&\n F = (a * b)"),
"Double|2"
);
assert_eq!(
run(" Dim a\n a = 2!\n F = (a - 0.5)"),
"Double|1.5"
);
}
#[test]
fn only_plus_short_circuits_past_a_bad_partner() {
assert_eq!(expr("IsNull(Null + \"Z\")"), "Boolean|True");
assert_eq!(expr("IsNull(\"Z\" + Null)"), "Boolean|True");
assert_eq!(expr("IsNull(Null + \"12\")"), "Boolean|True");
for e in [
"\"Z\" - Null",
"Null - \"Z\"",
"\"Z\" * Null",
"\"Z\" / Null",
"\"Z\" ^ Null",
"\"Z\" Mod Null",
"Null Mod \"Z\"",
"\"Z\" \\ Null",
"\"Z\" And Null",
"Null Or \"Z\"",
] {
assert_eq!(expr(e), "ERR|13", "for {e}");
}
assert_eq!(expr("\"Z\" & Null"), "String|Z");
assert_eq!(expr("IsNull(1 - Null)"), "Boolean|True");
assert_eq!(expr("IsNull(Null Mod 3)"), "Boolean|True");
}
#[test]
fn unary_sign_promotes_on_overflow_at_runtime() {
assert_eq!(
run(" Dim a\n a = 2147483647\n F = (-(Not a))"),
"Double|2147483648"
);
assert_eq!(
run(" Dim a\n a = 2147483647\n F = TypeName(-(Not a))"),
"String|Double"
);
assert_eq!(
run(" Dim a\n a = 32767\n F = (-(Not a))"),
"Long|32768"
);
}
#[test]
fn a_statically_boolean_select_subject_converts_its_cases_with_cbool() {
let sel = |subject: &str, cases: &str| {
format!(
" Dim r\n Select Case {subject}\n{cases} Case Else\n r = \"else\"\n End Select\n F = r"
)
};
let hit = |subject: &str, case: &str| {
run(&sel(
subject,
&format!(" Case {case}\n r = \"a\"\n"),
))
};
for subject in ["(1 = 1)", "True", "CBool(1)", "IsNumeric(0)"] {
assert_eq!(hit(subject, "1"), "String|a", "{subject} vs Case 1");
assert_eq!(hit(subject, "0"), "String|else", "{subject} vs Case 0");
assert_eq!(hit(subject, "0, 1"), "String|a", "{subject} vs Case 0, 1");
assert_eq!(hit(subject, "2 To 5"), "String|a", "{subject} vs 2 To 5");
assert_eq!(hit(subject, "0 To 1"), "String|else", "{subject} vs 0 To 1");
assert_eq!(hit(subject, "Is = 1"), "String|a", "{subject} vs Is = 1");
assert_eq!(hit(subject, "Is > 0"), "String|else", "{subject} vs Is > 0");
assert_eq!(hit(subject, "Is < 0"), "String|a", "{subject} vs Is < 0");
}
assert_eq!(hit("(1 = 2)", "0, 1"), "String|a");
assert_eq!(hit("(1 = 2)", "2 To 5"), "String|else");
assert_eq!(
run(&sel("CBool(1)", " Case Null\n r = \"a\"\n")),
"ERR|94"
);
let via_var = |value: &str, case: &str| {
run(&format!(
" Dim a, r\n a = {value}\n Select Case a\n Case {case}\n \
r = \"a\"\n Case Else\n r = \"else\"\n End Select\n F = r"
))
};
assert_eq!(via_var("True", "0, 1"), "String|else");
assert_eq!(via_var("True", "-1"), "String|a");
assert_eq!(via_var("True", "2 To 5"), "String|else");
assert_eq!(via_var("True", "Is < 0"), "String|a");
assert_eq!(via_var("False", "0, 1"), "String|a");
}
#[test]
fn a_constant_boolean_over_a_constant_string_folds_to_a_boolean() {
assert_eq!(expr("True Mod \"12\""), "Boolean|False");
assert_eq!(expr("True \\ \"12\""), "Boolean|True");
assert_eq!(expr("False \\ \"12\""), "Boolean|False");
assert_eq!(expr("True Mod \"0\""), "ERR|11");
assert_eq!(expr("True \\ \"0\""), "ERR|11");
assert_eq!(expr("\"12\" Mod True"), "Long|0");
assert_eq!(expr("\"12\" \\ True"), "Long|-12");
assert_eq!(expr("True Mod 12"), "Integer|-1");
assert_eq!(expr("True \\ 12"), "Integer|0");
assert_eq!(
run(" Dim a\n a = True\n F = (a Mod \"12\")"),
"Long|-1"
);
assert_eq!(
run(" Dim b\n b = \"12\"\n F = (True Mod b)"),
"Long|-1"
);
assert_eq!(expr("True And \"12\""), "Long|12");
assert_eq!(expr("True Or \"12\""), "Long|-1");
assert_eq!(expr("True Eqv \"12\""), "Long|12");
}
#[test]
fn integer_operators_process_the_left_operand_first() {
assert_eq!(
run(" Dim a\n a = \"32768100000\"\n F = (a Mod \"Double\")"),
"ERR|6"
);
assert_eq!(
run(" Dim a\n a = \"Double\"\n F = (a Mod \"32768100000\")"),
"ERR|13"
);
assert_eq!(
run(" Dim a\n a = \"32768100000\"\n F = (a Mod 3)"),
"ERR|6"
);
}
#[test]
fn every_intrinsic_handles_null_the_way_excel_does() {
for f in [
"CVar", "Abs", "Int", "Fix", "Round", "Len", "UCase", "LCase", "Trim", "LTrim",
"RTrim", "Hex", "Oct",
] {
assert_eq!(
expr(&format!("IsNull({f}(Null))")),
"Boolean|True",
"{f} should propagate"
);
}
for e in [
"Left(Null, 1)",
"Right(Null, 1)",
"Mid(Null, 1, 1)",
"InStr(Null, \"a\")",
"String(2, Null)",
"StrComp(Null, \"a\")",
] {
assert_eq!(
expr(&format!("IsNull({e})")),
"Boolean|True",
"{e} should propagate"
);
}
for f in [
"CStr",
"CInt",
"CLng",
"CDbl",
"CSng",
"CBool",
"CCur",
"Val",
"Sgn",
"Sqr",
"Exp",
"Log",
"Sin",
"Cos",
"Tan",
"Atn",
"Space",
"StrReverse",
"Chr",
"Asc",
] {
assert_eq!(expr(&format!("{f}(Null)")), "ERR|94", "{f} should reject");
}
assert_eq!(expr("Replace(Null, \"a\", \"b\")"), "ERR|94");
assert_eq!(expr("TypeName(Null)"), "String|Null");
assert_eq!(expr("IsNull(Null)"), "Boolean|True");
assert_eq!(expr("IsNumeric(Null)"), "Boolean|False");
assert_eq!(expr("IsEmpty(Null)"), "Boolean|False");
}
#[test]
fn conversions_reject_null_rather_than_propagating_it() {
assert_eq!(expr("CStr(Null)"), "ERR|94");
assert_eq!(expr("CDbl(Null)"), "ERR|94");
assert_eq!(expr("CLng(Null)"), "ERR|94");
assert_eq!(expr("IsNull(UCase(Null))"), "Boolean|True");
assert_eq!(expr("IsNull(Left(Null, 1))"), "Boolean|True");
assert_eq!(expr("TypeName(Null)"), "String|Null");
assert_eq!(expr("IsNull(Null)"), "Boolean|True");
}
#[test]
fn the_words_true_and_false_coerce_on_the_integer_path_only() {
assert_eq!(expr("\"True\" Xor 1"), "Integer|-2");
assert_eq!(expr("\"False\" Xor 1"), "Integer|1");
assert_eq!(expr("\"True\" \\ 1"), "Integer|-1");
assert_eq!(expr("\"True\" Mod 2"), "Integer|-1");
assert_eq!(expr("CBool(\"True\")"), "Boolean|True");
assert_eq!(expr("\"true\" Xor 1"), "Integer|-2");
assert_eq!(expr("\"TRUE\" Xor 1"), "Integer|-2");
assert_eq!(expr("Not \"True\""), "Boolean|False");
assert_eq!(expr("True Eqv \"True\""), "ERR|13");
assert_eq!(expr("\"True\" Eqv True"), "ERR|13");
assert_eq!(expr("True Eqv CStr(True)"), "ERR|13");
assert_eq!(
run(" Dim a\n a = 3.75\n F = (IsNumeric(a) Eqv CStr(True))"),
"ERR|13"
);
assert_eq!(expr("LCase(\"TRUE\") Eqv True"), "Boolean|True");
assert_eq!(expr("LCase(False) Eqv IsNull(True)"), "Boolean|True");
assert_eq!(
run(" Dim a\n a = True\n F = (a Eqv \"True\")"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = \"false\"\n F = (a Eqv False)"),
"Boolean|True"
);
assert_eq!(
run(" Dim a\n a = \"true\"\n F = (a Eqv False)"),
"Boolean|False"
);
assert_eq!(
run(" Dim a, b\n a = \"true\"\n b = False\n F = (a Eqv b)"),
"Boolean|False"
);
for e in [
"\"True\" + 1",
"\"False\" + 1",
"\"True\" * 2",
"CDbl(\"True\")",
] {
assert_eq!(expr(e), "ERR|13", "for {e}");
}
assert_eq!(expr("IsNumeric(\"True\")"), "Boolean|False");
assert_eq!(expr("Trim((1 >= 2)) Xor 5"), "Integer|5");
}
#[test]
fn a_string_outside_double_range_fails_to_convert() {
assert_eq!(
run(" Dim a\n a = \"1E+2923\"\n F = (a ^ 255)"),
"ERR|6"
);
assert_eq!(
run(" Dim a\n a = \"1E400\"\n F = (a + 1)"),
"ERR|6"
);
assert_eq!(
run(" Dim a\n a = \"255\"\n F = (a ^ 255)"),
"Double|INF"
);
assert_eq!(
run(" Dim a\n a = 255\n F = (a ^ 255)"),
"Double|INF"
);
}
#[test]
fn an_empty_string_never_coerces_to_a_number() {
for e in [
"\"\" - 3",
"\"\" + 3",
"\"\" * 3",
"\"\" \\ 3",
"Not \"\"",
"CDbl(\"\")",
] {
assert_eq!(expr(e), "ERR|13", "for {e}");
}
}
#[test]
fn val_always_returns_a_double() {
assert_eq!(expr("Val(255)"), "Double|255");
assert_eq!(expr("Val(\"1.5\")"), "Double|1.5");
assert_eq!(expr("Val(\"100000\")"), "Double|100000");
assert_eq!(run(" Dim a\n a = 1%\n F = Val(a)"), "Double|1");
}
#[test]
fn a_zero_base_with_a_negative_exponent_is_an_error() {
assert_eq!(
run(" Dim a, b\n a = 0\n b = -1\n F = (a ^ b)"),
"ERR|5"
);
assert_eq!(
run(" Dim a, b\n a = 0\n b = -246\n F = (a ^ b)"),
"ERR|5"
);
assert_eq!(
run(" Dim a, b\n a = 0\n b = 0\n F = (a ^ b)"),
"Double|1"
);
assert_eq!(
run(" Dim a, b\n a = 0\n b = 2\n F = (a ^ b)"),
"Double|0"
);
assert_eq!(
run(" Dim a, b\n a = 2\n b = -2\n F = (a ^ b)"),
"Double|0.25"
);
}
#[test]
fn logical_operators_range_check_their_operands_too() {
assert_eq!(expr("True Or \"2147483648\""), "ERR|6");
assert_eq!(expr("1 And \"2147483648\""), "ERR|6");
assert_eq!(expr("True Or \"3.752147483647\""), "Long|-1");
assert_eq!(expr("1 And \"12\""), "Long|0");
}
#[test]
fn int_div_and_mod_range_check_their_operands_not_just_the_result() {
assert_eq!(
run(" Dim a, b\n a = 254\n b = \"22147483647\"\n F = (a Mod b)"),
"ERR|6"
);
assert_eq!(
run(" Dim a, b\n a = 254\n b = \"22147483647\"\n F = (a \\ b)"),
"ERR|6"
);
assert_eq!(
run(" Dim a, b\n a = 3000000000#\n b = 3\n F = (a Mod b)"),
"ERR|6"
);
assert_eq!(
run(" Dim a, b\n a = 254\n b = 2147483647\n F = (a Mod b)"),
"Long|254"
);
assert_eq!(
run(" Dim a, b\n a = 40000\n b = 3\n F = (a Mod b)"),
"Long|1"
);
assert_eq!(
run(" Dim a, b\n a = 40000\n b = 3\n F = (a \\ b)"),
"Long|13333"
);
}
#[test]
fn a_negative_base_with_a_fractional_exponent_is_an_error() {
assert_eq!(expr("(-1) ^ 1.5"), "ERR|5");
assert_eq!(expr("(-8) ^ (1 / 3)"), "ERR|5");
assert_eq!(expr("(-2) ^ 2"), "Double|4");
assert_eq!(expr("(-2) ^ 3"), "Double|-8");
}
#[test]
fn select_case_matches_a_numeric_case_against_a_string_subject() {
let body = |x: &str| {
format!(
" Dim r\n Select Case {x}\n Case 0\n r = \"zero\"\n \
Case 10\n r = \"ten\"\n Case Else\n r = \"else\"\n \
End Select\n F = r"
)
};
assert_eq!(run(&body("\"10\"")), "String|ten");
assert_eq!(run(&body("\"\"")), "String|else");
}
#[test]
fn a_for_counter_is_left_at_the_value_that_failed_the_test() {
assert_eq!(
run(" Dim c\n For c = 1 To 3\n Next c\n F = c"),
"Integer|4"
);
assert_eq!(
run(" Dim c\n For c = 1 To 3 Step 2\n Next c\n F = c"),
"Integer|5"
);
assert_eq!(
run(" Dim c\n For c = 5 To 1\n Next c\n F = c"),
"Integer|5"
);
assert_eq!(
run(" Dim c\n For c = 3 To 1 Step -1\n Next c\n F = c"),
"Integer|0"
);
assert_eq!(
run(" Dim c\n For c = 1 To 3\n Exit For\n Next c\n F = c"),
"Integer|1"
);
}
#[test]
fn count_arguments_round_rather_than_truncate() {
assert_eq!(expr("Len(Space(2.6))"), "Long|3");
assert_eq!(expr("Space(-1)"), "ERR|5");
assert_eq!(expr("String(-1, \"x\")"), "ERR|5");
assert_eq!(expr("Left(\"abc\", -1)"), "ERR|5");
assert_eq!(expr("Right(\"abc\", 99)"), "String|abc");
assert_eq!(expr("InStr(0, \"abc\", \"b\")"), "ERR|5");
assert_eq!(expr("String(2, 65)"), "String|AA");
}
#[test]
fn host_object_access_errors_rather_than_silently_doing_nothing() {
for body in [
" F = Range(\"A1\").Value",
" F = ThisWorkbook.Name",
" F = Worksheets(1).Name",
" F = Application.WorksheetFunction.Sum(1, 2)",
" Dim c\n For Each c In r\n Next",
] {
let out = run(body);
assert!(out.starts_with("ERR|438"), "{body:?} gave {out}");
}
}
#[test]
fn a_member_of_a_non_object_is_error_424() {
assert_eq!(run(" With x\n F = .a\n End With"), "ERR|424");
assert_eq!(expr("x.Name"), "ERR|424");
assert_eq!(expr("x Is Nothing"), "ERR|424");
}
#[test]
fn an_unknown_function_is_reported_not_ignored() {
assert_eq!(expr("NoSuchFunction(1)"), "ERR|35");
}
}