Overview
Expression evaluation is the core of vb6interpret's execution engine. It takes a
Concrete Syntax Tree (CST) node produced by vb6parse
and reduces it to a runtime value represented as a
VB6Variant from
vb6runtime.
The evaluator operates as a tree-walk interpreter: it recursively descends the CST, evaluating each expression node according to its kind. Pure operator semantics are separated from the interpreter logic, allowing the operator functions to be tested and reasoned about independently of any CST or interpreter state.
VB6Variant. Only statement execution (assignments, control flow, etc.)
mutates state. This separation makes the operators module a pure library.
Architecture
Design Philosophy
The expression evaluator is built on three architectural principles:
- Purity: Operator functions (
arith,add,bitwise,compare_ord,like_match,literal_value) takeVB6Variantvalues and return new ones. They know nothing about CST nodes, scopes, or the interpreter. They operate on values alone. - Separation of Concerns: The
evalmodule handles CST dispatch, scope lookup, and error positioning. Theoperatorsmodule handles VB6-specific numeric and logical semantics. Theliteralsmodule handles VB6 literal syntax. Thelikemodule handles pattern matching. - VB6 Fidelity: Every operator implements the exact semantics documented in the VB6
language reference: Euclidean integer division, always-double exponentiation, case-insensitive
string comparison, and the dual behavior of
+(addition vs. concatenation).
Module Structure
The expression evaluation subsystem lives in
projects/vb6interpret/src/eval/ and is organized into four source files:
| File | Lines | Responsibility |
|---|---|---|
mod.rs |
367 | CST dispatch table, scope lookup, call expression dispatch, error positioning |
operators.rs |
348 | Pure arithmetic, logical/bitwise, and comparison functions over VB6Variant |
literals.rs |
248 | Pure literal parsing: strings, numbers, dates, booleans, radix notation |
like.rs |
198 | Pure VB6 Like pattern matching with memoized recursion |
operators, literals, and like
modules are fully unit-testable in isolation. Their test suites cover edge cases including overflow
behavior, null propagation, case sensitivity, and Unicode handling.
Integration with the Interpreter
Expression evaluation is invoked from two places in the statement execution pipeline:
// From exec/assignment.rs — evaluating the RHS of an assignment
let value = self.eval_expr(rhs)?;
self.assign(lhs, value)
// From exec/assignment.rs — evaluating a Let statement
let value = self.eval_expr(rhs)?;
self.assign(lhs, value)
// From eval/mod.rs — evaluating a CallExpression
let args = self.eval_args(list)?;
builtins::call_builtin(&name, &args)
The data flow follows this pipeline:
Concrete Syntax Tree
eval_expr dispatch
Runtime Value
Literal Evaluation
Literal evaluation converts raw token text into VB6Variant values. The function
literal_value(text, kind) in literals.rs is the single entry point,
dispatching on SyntaxKind to the appropriate parser.
String Literals
String literals are unescaped by replacing doubled quote characters ("") with single
quotes ("), matching VB6's convention:
' Source code
Dim s As String
s = "She said ""Hello"""
' After evaluation:
' s = "She said "Hello""
SyntaxKind::StringLiteral => {
let inner = raw.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))?;
let unescaped = inner.replace("\"\"", "\"");
Some(VBVariant::from_string(unescaped))
}
Numeric Literals
Numeric literals are parsed by kind, with type suffixes stripped before conversion. The parser
handles decimal, hexadecimal (&H), and octal (&O) notation:
' Decimal integers
Dim a As Integer: a = 42% ' Integer suffix
Dim b As Long: b = 100& ' Long suffix
' Hexadecimal
Dim c As Long: c = &HFF ' 255 in decimal
Dim d As Integer: d = &h7F ' 127 in decimal
' Octal
Dim e As Long: e = &O17 ' 15 in decimal
Radix Literals
Radix literals (hex and octal) follow VB6's two's-complement wrapping rules. Values above
i32::MAX but within u32::MAX wrap to their signed equivalent:
fn radix_value(digits: &str, radix: u32) -> Option<VB6Variant> {
let v = i64::from_str_radix(digits, radix).ok()?;
// &HFFFFFFFF wraps to -1 Long in VB6
if v > i32::MAX as i64 && v <= u32::MAX as i64 {
return Some(VB6Variant::Long(v as i32)); // -1
}
Some(VB6Variant::from_i64(v))
}
Radix Wrapping Examples
| Literal | Raw Value | Evaluated Result |
|---|---|---|
&HFFFFFFFF |
4,294,967,295 | VB6Variant::Long(-1) |
&H80000000 |
2,147,483,648 | VB6Variant::Long(-2,147,483,648) |
&H7FFFFFFF |
2,147,483,647 | VB6Variant::Long(2,147,483,647) |
&H100000000 |
4,294,967,296 | VB6Variant::Double(4294967296.0) |
Type Suffixes
VB6 supports single-character type declaration suffixes that are stripped before numeric parsing:
| Suffix | Type | Example | Rust Type |
|---|---|---|---|
% |
Integer | 42% |
i16 |
& |
Long | 100& |
i32 |
! |
Single | 3.14! |
f32 |
# |
Double | 3.14# |
f64 |
@ |
Currency | 12.34@ |
i64 (scaled) |
Date Literals
Date literals are delimited by # characters and parsed into date serial values:
Dim d As Date
d = #1/1/2000# ' January 1, 2000
d = #12:00:00# ' Midnight
d = #6/30/2024 3:30PM#
SyntaxKind::DateLiteral => {
let inner = raw.strip_prefix('#')
.and_then(|rest| rest.strip_suffix('#'))?;
VBVariant::from_string(inner)
.as_date_serial()
.ok()
.map(VB6Variant::Date)
}
Boolean Keywords
The True and False keywords are evaluated at the literal level:
SyntaxKind::TrueKeyword => Some(VB6Variant::Boolean(true)),
SyntaxKind::FalseKeyword => Some(VB6Variant::Boolean(false)),
Identifier Evaluation
Identifier evaluation resolves a name to its current value from the interpreter's scope. Special VB6 keywords are handled as a first pass before the scope lookup:
fn eval_identifier(&mut self, node: &CstNode) -> RunResult<VB6Variant> {
let name = identifier_name(node);
match name.to_lowercase().as_str() {
"true" => return Ok(VB6Variant::Boolean(true)),
"false" => return Ok(VB6Variant::Boolean(false)),
"nothing" => return Ok(VB6Variant::Nothing),
"null" => return Ok(VB6Variant::Null),
"empty" => return Ok(VB6Variant::Empty),
"me" => /* error: not available in standard module */,
_ => {}
}
// Scope lookup: current frame locals, then globals
match self.lookup(&name) {
Some(value) => Ok(value.clone()),
None => Ok(VB6Variant::Empty), // undeclared → Empty
}
}
Special Keywords
The evaluator recognizes six special keywords that are not regular variables:
| Keyword | Evaluated Value | Description |
|---|---|---|
True |
VB6Variant::Boolean(true) |
Boolean true |
False |
VB6Variant::Boolean(false) |
Boolean false |
Nothing |
VB6Variant::Nothing |
Null object reference |
Null |
VB6Variant::Null |
No valid data |
Empty |
VB6Variant::Empty |
Uninitialized variable |
Me |
Runtime error | Object reference (not supported in standard modules) |
Scope Lookup
The lookup function searches for a variable in a two-level scope chain. All lookups
are case-insensitive (normalized via normalize()):
fn lookup(&self, name: &str) -> Option<&VB6Variant> {
let key = normalize(name);
// 1. Check current procedure's frame locals
if let Some(frame) = self.frames.last()
&& let Some(value) = frame.locals.get(&key)
{
return Some(value);
}
// 2. Check module-level globals
self.globals.get(&key)
}
Undeclared Variables
When Option Explicit is not set, VB6 allows referencing undeclared variables. The
interpreter follows VB6 semantics by returning Empty for such references:
' Without Option Explicit, this is valid:
Sub Test()
Dim x As Integer
x = y ' y is undeclared → evaluates to Empty (0 numerically)
Print x ' Prints 0
End Sub
Empty variant, which coerces to 0 for numeric
contexts and "" for string contexts.
Binary Operators
Binary expressions are evaluated by eval_binary, which first evaluates both operands
and then dispatches to the appropriate operator function based on the operator token kind. There
are 20+ binary operator variants:
Arithmetic
+— Addition / String concat-— Subtraction*— Multiplication/— Division\— Integer divisionMod— Modulo^— Exponentiation
String
&— Concatenation
Equality
=— Equality<>— Inequality
Ordered Comparison
<— Less than<=— Less than or equal>— Greater than>=— Greater than or equal
Logical / Bitwise
AndOrXorEqvImp
Special
Like— Pattern matchingIs— Object identity
Arithmetic Operators
All arithmetic operations flow through the arith function, which implements a
two-path algorithm: an integer path for exact integral results, and a floating-point path for
everything else.
Integer Arithmetic Path
When both operands are integral types and the operator is not division or exponentiation, the
evaluator uses i64 arithmetic with checked operations:
pub(crate) fn arith(
lhs:VBVariant, rhs:VBVariant, op: ArithmeticOperator
) -> VBResult<VB6Variant> {
// Integer path: both operands must be integral
if op != ArithmeticOperator::Divide
&& op != ArithmeticOperator::Exponent
&& lhs.is_integral()
&& rhs.is_integral()
{
let li = lhs.as_i64().ok();
let ri = rhs.as_i64().ok();
if let (Some(left), Some(right)) = (li, ri) {
let result = match op {
ArithmeticOperator::Add => left.checked_add(right),
ArithmeticOperator::Subtract => left.checked_sub(right),
ArithmeticOperator::Multiply => left.checked_mul(right),
ArithmeticOperator::IntegerDivide => {
if right == 0 { return Err(division_by_zero()); }
Some(left.div_euclid(right)) // Euclidean division
}
ArithmeticOperator::Modulus => {
if right == 0 { return Err(division_by_zero()); }
Some(left.rem_euclid(right)) // Euclidean modulo
}
_ => None,
};
if let Some(value) = result {
return Ok(VB6Variant::from_i64(value));
}
}
}
// Fallback to floating-point path below...
}
\ uses Euclidean
semantics, where the remainder always has the same sign as the divisor. This means:
-7 \ 2 = -4 (not -3), and -7 Mod 2 = 1. This matches VB6's behavior.
Floating-Point Arithmetic Path
When either operand is non-integral, or when the operator is division or exponentiation, the
operands are coerced to f64 and operations proceed in floating-point:
let left = lhs.as_f64()?;
let right = rhs.as_f64()?;
let result = match op {
ArithmeticOperator::Add => left + right,
ArithmeticOperator::Subtract => left - right,
ArithmeticOperator::Multiply => left * right,
ArithmeticOperator::Divide => left / right,
ArithmeticOperator::IntegerDivide => (left / right).floor(),
ArithmeticOperator::Modulus => left % right,
ArithmeticOperator::Exponent => left.powf(right),
};
Ok(VB6Variant::from_double(result))
^ (exponentiation) operator always produces a
Double, even when both operands are integers. This is explicit in the code:
ArithmeticOperator::Exponent is excluded from the integer path.
Integer Overflow Fallback
When checked integer arithmetic overflows, the evaluator silently falls back to the floating-point path:
Dim a As Long
Dim b As Long
a = 9223372036854775807 ' i64::MAX
b = 1
' a + b overflows → silently becomes Double(9223372036854775808.0)
The + Operator
The + operator has dual semantics in VB6: it performs numeric addition when both operands
are numeric, and string concatenation when both operands are strings. A type mismatch is raised when
one operand is a string and the other is numeric:
pub(crate) fn add(lhs:VBVariant, rhs:VBVariant) -> VBResult<VB6Variant> {
match (&lhs, &rhs) {
// String + String → concatenation
(VB6Variant::String(_),VBVariant::String(_)) => {
let left = lhs.as_string()?;
let right = rhs.as_string()?;
Ok(VB6Variant::from_string(format!("{left}{right}")))
}
// String + Numeric or Numeric + String → type mismatch
(VB6Variant::String(_), _) | (_,VBVariant::String(_)) => Err(VBError::type_mismatch()),
// Both numeric → arithmetic path
_ => arith(lhs, rhs, ArithmeticOperator::Add),
}
}
' Addition
Dim x As Integer
x = 3 + 5 ' 8
' Concatenation
Dim a As String, b As String
a = "Hello"
b = " World"
Print a + b ' "Hello World"
' Type mismatch error
Dim c As String
c = "42" + 1 ' Type mismatch: can't add string and number
String Concatenation
The & operator always concatenates strings, performing implicit coercion of non-string
operands:
SyntaxKind::Ampersand => {
let left = lhs.as_string()?;
let right = rhs.as_string()?;
Ok(VB6Variant::from_string(format!("{left}{right}")))
}
Dim x As Integer
x = 42
Print "The answer is: " & x ' "The answer is: 42"
Print 10 & 20 ' "1020" (always string concatenation)
+, the & operator never raises a
type mismatch. It coerces all operands to strings before concatenation.
Comparison Operators
Comparison operators use compare_ord, which dispatches to case-insensitive string
comparison when both operands are strings, or numeric comparison otherwise:
pub(crate) fn compare_ord(
lhs:VBVariant, rhs:VBVariant, ord: Ordering
) -> VBResult<VB6Variant> {
let ordering = match (&lhs, &rhs) {
// String comparison: case-insensitive (Option Compare Text)
(VB6Variant::String(left),VBVariant::String(right)) => {
compare_strings(left, right)
}
// Numeric comparison: magnitude-based
_ => match (lhs.as_f64(), rhs.as_f64()) {
(Ok(left), Ok(right)) => left.partial_cmp(&right),
_ => return Err(VBError::type_mismatch()),
},
};
let result = matches!(
(ordering, ord),
(Some(std::cmp::Ordering::Less), Ordering::Less)
| (Some(std::cmp::Ordering::Equal), Ordering::LessOrEqual)
// ...
);
Ok(VB6Variant::Boolean(result))
}
Case-Insensitive String Comparison
VB6's default Option Compare is Text (case-insensitive). All string
comparisons are case-insensitive unless overridden:
' Case-insensitive comparisons
Dim a As String, b As String
a = "apple"
b = "Apple"
Print a < b ' True (case-insensitive)
Print a = b ' True (case-insensitive)
Logical and Bitwise Operators
The operators And, Or, Xor, Eqv, and
Imp are handled by bitwise, which dispatches based on operand types:
- Boolean operands: Pure logical operations yielding
Boolean - Integral operands: Bitwise operations on the integral value, yielding the integral type
pub(crate) fn bitwise(
lhs:VBVariant, rhs:VBVariant, op: LogicalOperator
) -> VBResult<VB6Variant> {
// Boolean → logical combination
if let (VB6Variant::Boolean(left),VBVariant::Boolean(right)) = (&lhs, &rhs) {
let result = match op {
LogicalOperator::And => *left && *right,
LogicalOperator::Or => *left || *right,
LogicalOperator::Xor => *left != *right,
LogicalOperator::Eqv => *left == *right,
LogicalOperator::Imp => !*left || *right,
};
return Ok(VB6Variant::Boolean(result));
}
// Integral → bitwise operation
let left = lhs.as_i64()?;
let right = rhs.as_i64()?;
let result = match op {
LogicalOperator::And => left & right,
LogicalOperator::Or => left | right,
LogicalOperator::Xor => left ^ right,
LogicalOperator::Eqv => !(left ^ right),
LogicalOperator::Imp => !left | right,
};
Ok(VB6Variant::from_i64(result))
}
' Boolean logic
Dim a As Boolean, b As Boolean
a = True: b = False
Print a And b ' False
Print a Or b ' True
Print a Xor b ' True
Print a Eqv b ' False (not equivalent)
Print a Imp b ' False (True implies False is False)
' Bitwise operations
Dim x As Long, y As Long
x = 6 ' 0110 binary
y = 3 ' 0011 binary
Print x And y ' 2 (0010)
Print x Or y ' 7 (0111)
Print x Xor y ' 5 (0101)
The Like Operator
The Like operator performs VB6-style pattern matching using the pure like_match
function. It is case-insensitive and supports *, ?, #, and
[charlist] patterns without using regex:
Dim s As String
s = "Hello123"
Print s Like "Hello*" ' True
Print s Like "?????" ' False (7 chars required)
Print s Like "#####"," ' False (contains letters)
Print s Like "[A-Z]*" ' True
Print s Like "[!0-9]*" ' True (starts with non-digit)
Like implementation uses memoized recursive
descent (dynamic programming), not regular expressions. This avoids exponential backtracking on
patterns with multiple * wildcards.
The Is Operator
The Is operator tests object identity. Since vb6interpret does not yet
implement the object model, it falls back to value equality:
SyntaxKind::IsKeyword => {
let result = match (&lhs, &rhs) {
(VB6Variant::Nothing,VBVariant::Nothing) => true,
(VB6Variant::Nothing, _) | (_,VBVariant::Nothing) => false,
_ => lhs == rhs, // No object model yet: value equality
};
Ok(VB6Variant::Boolean(result))
}
Unary Operators
Unary expressions are evaluated by eval_unary, supporting three operators:
| Operator | SyntaxKind | Behavior |
|---|---|---|
- |
SubtractionOperator |
Negation: -value |
+ |
AdditionOperator |
Unary plus: +value (no-op) |
Not |
NotKeyword |
Logical NOT: Not bool |
fn eval_unary(&mut self, op: &CstNode, operand: &CstNode) -> RunResult<VB6Variant> {
let value = self.eval_expr(operand)?;
match op.kind() {
SyntaxKind::SubtractionOperator => {
let number = value.as_f64()?;
Ok(VB6Variant::from_double(-number))
}
SyntaxKind::AdditionOperator => {
let number = value.as_f64()?;
Ok(VB6Variant::from_double(number))
}
SyntaxKind::NotKeyword => {
let boolean = value.as_bool()?;
Ok(VB6Variant::Boolean(!boolean))
}
// ...
}
}
Dim x As Integer
x = 5
Print -x ' -5
Print +x ' 5
Dim b As Boolean
b = True
Print Not b ' False
Call Expressions
Three-Tier Dispatch
Call expressions (eval_call) use a three-tier dispatch strategy. Given a call node, the
evaluator attempts each tier in order:
Lookup + Index
Procedure Lookup
Registry Dispatch
fn eval_call(&mut self, node: &CstNode) -> RunResult<VB6Variant> {
let name = identifier_name(node);
let args = match argument_list {
Some(list) => self.eval_args(list)?,
None => Vec::new(),
};
// Tier 1: Array reference / element access
if let Some(VB6Variant::Array(_)) = self.lookup(&name) {
// ... handle indexing
}
// Tier 2: User-defined function
let key = normalize(&name);
if self.procedures.contains_key(&key) {
return self.call_function(&name, args);
}
// Tier 3: Built-in function
builtins::call_builtin(&name, &args)
}
Array Indexing
When the name resolves to an array variable, the call expression performs array indexing. An empty
argument list (arr()) returns the entire array; any argument list indexes into the array:
// Array reference: arr() returns the whole array
if args.is_empty() {
return Ok(array.clone());
}
// Element indexing: arr(0, 1) returns element at indices
let indices: Vec<i32> = args.iter().map(|arg| arg.as_i32()).collect()?;
let element = array.get(&indices)?;
Ok(element.clone())
Dim numbers(10) As Integer
numbers(0) = 42
numbers(5) = 100
Dim val As Integer
val = numbers(0) ' 42
Print numbers(5) ' 100
User-Defined Functions
When the name resolves to a procedure in the loaded program, call_function is invoked:
Function Add(a As Integer, b As Integer) As Integer
Add = a + b
End Function
Sub Main()
Dim result As Integer
result = Add(3, 5) ' Calls user-defined function
Print result ' 8
End Sub
The call framework pushes a new stack frame, binds arguments to parameters with type coercion, executes the procedure body, and pops the frame to retrieve the return value.
Built-in Functions
When the name does not resolve to a variable or user procedure, it is dispatched to the built-in function registry. The registry contains all implemented VB6 standard library functions, organized into categories:
| Category | Module | Examples |
|---|---|---|
| String | string.rs |
Left$, Mid$, Len, LCase$ |
| Conversion | conversion.rs |
CInt, CDbl, CStr, CBool |
| Math | math.rs |
Sqr, Rnd, Abs, Fix |
| Date/Time | datetime.rs |
Date, Time, Now, DateAdd |
| Interaction | interaction.rs |
MsgBox, InputBox, Beep |
| Type Checking | type_checking.rs |
IsArray, IsNull, IsEmpty |
| File | file.rs |
FileLen, LOF |
| Financial | financial.rs |
PV, FV, Pmt |
| Graphics | graphics.rs |
QBColor |
| Resources | resources.rs |
LoadRes, LoadResString |
| Environment | environment.rs |
Environ$ |
| Logic | logic.rs |
Switch |
| Objects | objects.rs |
CreateObject |
| Arrays | arrays.rs |
Filter, GetUpper |
Argument Evaluation
Arguments are evaluated left-to-right before any function body executes. Each argument expression is
reduced to a VB6Variant:
fn eval_args(&mut self, node: &CstNode) -> RunResult<Vec<VB6Variant>> {
let mut values = Vec::new();
for argument in node.children_by_kind(SyntaxKind::Argument) {
if let Some(expr) = argument.children()
.find(|child| !matches!(child.kind(), SyntaxKind::Comma))
{
values.push(self.eval_expr(expr)?);
}
}
Ok(values)
}
Built-in Function Dispatch
The built-in function system uses a registry-based approach where each function category registers
its functions in its own submodule. Adding a new function requires a single Builtin
entry in the category's register function.
Declarative Specifications
Each built-in is specified declaratively with its name, minimum/maximum argument count, and the
type of every parameter. The typed_builtin! macro expands this specification into a
type-converted function call adapter.
The typed_builtin! Macro
The macro takes a function name, argument count bounds, parameter declarations, and a body. It generates glue code that:
- Iterates over the raw
&[VB6Variant]argument slice - Converts each argument to the declared type via
vb6runtime::boundary - Attaches parameter index and name information for error reporting
- Invokes the body with the converted parameters
- Returns a
VResult<VB6Variant>
// Declarative specification for Left$
registry.insert(typed_builtin!("left$", 2, 2,
(input: string, length: long),
strfn::left_dollar(&input, &length).map(VB6Variant::from))
);
Parameter Kinds
Each parameter kind maps to a VB6 declared type and determines how the argument is converted at the boundary:
| Kind | VB6 Type | Conversion |
|---|---|---|
variant |
As Variant |
Raw &VB6Variant (no coercion) |
string |
As String |
VString |
long |
As Long |
VLong |
integer |
As Integer |
VBInteger |
boolean |
As Boolean |
VBoolean |
single |
As Single |
VBSingle |
double |
As Double |
VDouble |
currency |
As Currency |
VBCurrency |
date |
As Date |
VBDate |
propstring |
As String |
Null-propagating string |
Null Propagation
Certain parameter kinds support null propagation: if a Null argument is passed, the
builtin call short-circuits to return Null without invoking the function body:
($args:ident, $index:expr, propstring) => {{
if matches!($args.get($index), Some(VB6Variant::Null)) {
return Ok(VB6Variant::Null);
}
::vb6runtime::boundary::arg::<::vb6runtime::value::VString>($args, $index)
}};
' Null propagation
Dim s As String, result As String
s = Null
result = Left$(s, 2) ' Returns Null, not ""
Print IsNull(result) ' True
VB6 Like Pattern Matching
The Like operator in VB6 performs pattern matching against strings using a small
pattern language. The implementation in like.rs is a pure, memoized recursive descent
parser — no regular expressions are used.
Supported Patterns
| Pattern | Matches | Example |
|---|---|---|
* |
Zero or more characters | "Hello*" → "Hello", "HelloWorld" |
? |
Exactly one character | "H?llo" → "Hello", "Hallo" |
# |
Exactly one digit (0-9) | "###" → "123", "007" |
[charlist] |
Any single character in charlist | "[A-Z]" → "A", "Z" |
[!charlist] |
Any single character NOT in charlist | "[!0-9]" → "A", "z" |
Character Classes
Character classes support ranges via the - separator and case-insensitive matching:
Dim s As String
s = "aB3c"
Print s Like "[A-Za-z0-9]" ' True (single alphanumeric char)
Print s Like "[a-z]" ' True (case-insensitive)
Print s Like "[!a-z]" ' True (matches '3')
Bracket Idioms
VB6 uses several idioms for matching literal bracket characters inside character classes:
| Pattern | Matches | Explanation |
|---|---|---|
[]] |
] |
A ] immediately after [ is a literal member |
[!]] |
Any char except ] |
Negated form: everything except literal ] |
[[] |
[ |
A [ inside a class is literal |
[?] |
? |
Special characters inside brackets are literal |
Memoized Matching Algorithm
The matching algorithm uses memoization to avoid exponential backtracking on patterns with
overlapping * branches:
pub(crate) fn like_match(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
// Memoization table: pat.len() × text.len()
let mut memo = vec![vec![None; txt.len() + 1]; pat.len() + 1];
like_match_at(&pat, &txt, 0, 0, &mut memo)
}
fn like_match_at(
pat: &[char], txt: &[char],
pat_idx: usize, text_idx: usize,
memo: &mut Vec<Vec<Option<bool>>>,
) -> bool {
// Check memoization table first
if let Some(cached) = memo[pat_idx][text_idx] {
return cached;
}
let result = if pat_idx == pat.len() {
text_idx == txt.len()
} else if pat[pat_idx] == '*' {
// Zero-or-more: try skipping * OR consuming a character
like_match_at(pat, txt, pat_idx + 1, text_idx, memo)
|| (text_idx < txt.len() && like_match_at(pat, txt, pat_idx, text_idx + 1, memo))
} else if pat[pat_idx] == '[' {
// Character class matching...
} else {
// Literal or ? or # matching...
};
memo[pat_idx][text_idx] = Some(result);
result
}
*a*a* matching
against "xaxa" would explore exponentially many branches. The memoized version runs
in O(pat.len × text.len) time.
Type Coercion and Variant Behavior
Variant Representation
All runtime values flow through VB6Variant,
the variant-like value type from vb6runtime. It supports the following types:
| VB6Variant Kind | Corresponds to VB6 Type |
|---|---|
VB6Variant::Integer(i16) |
Integer |
VB6Variant::Long(i32) |
Long |
VB6Variant::Single(f32) |
Single |
VB6Variant::Double(f64) |
Double |
VB6Variant::Currency(i64) |
Currency |
VB6Variant::Boolean(bool) |
Boolean |
VB6Variant::String(String) |
String |
VB6Variant::Date(f64) |
Date |
VB6Variant::Array(Array) |
Array |
VB6Variant::Empty |
Empty |
VB6Variant::Null |
Null |
VB6Variant::Nothing |
Nothing (object reference) |
Implicit Coercion
The evaluator implicitly coerces values at type boundaries. When a numeric operator needs a numeric
operand, VB6Variant provides conversion methods (as_f64, as_i64,
as_string) that handle the variant's internal type:
// In eval_binary — all numeric operations use as_f64()
let lhs = self.eval_expr(left)?;
let rhs = self.eval_expr(right)?;
let left = lhs.as_f64()?; ' i64 → f64, f32 → f64, etc.
let right = rhs.as_f64()?;
Null Handling
VB6's Null value propagates through most operations: any operation involving Null
returns Null. The interpreter handles this at the coercion boundary:
Dim x As Variant
x = Null
Print x + 1 ' Null (propagates)
Print x & "abc" ' "Null" (string path coerces)
Print x = 5 ' Null (comparison with Null yields Null)
Error Handling
Runtime Errors
Expression evaluation errors are wrapped in RunError, which carries:
- The underlying
VErrorwith an error number and description - The source line number of the statement containing the expression
- The name of the currently executing procedure
- Optional built-in call parameter information for argument mismatch errors
fn error_at(
&self, node: &CstNode, error: VError,
call_info: Option<BuiltinCallInfo>,
) -> RunError {
RunError::new(error)
.at_line(self.current_stmt_line)
.in_procedure(&self.current_procedure_name())
.with_builtin_call(call_info)
}
Source Positioning
While expression evaluation is pure, error positioning relies on the interpreter's state: the current
statement line and procedure name. The error_at method attaches this location context
to the error.
Sub Calculate()
Dim x As Integer
x = 1 / 0 ' Runtime error 11 (Division by zero)
' Error reports: line 4, procedure "Calculate"
End Sub
End-to-End Example
Consider this VB6 program and trace how each expression is evaluated:
' Example program
Sub Main()
Dim a As Integer
Dim b As Integer
Dim result As Double
a = 10
b = 3
result = a \ b ' Integer division: 3
result = a / b ' Floating-point: 3.333...
result = a ^ 2 ' Exponentiation: 100.0
result = a & Mod b ' Modulo: 1
Print "Done:" & result
End Sub
The evaluation trace:
| Expression | Expression Kind | Evaluation Path | Result |
|---|---|---|---|
10 |
IntegerLiteral |
eval_literal → literal_value |
VB6Variant::Long(10) |
3 |
IntegerLiteral |
eval_literal → literal_value |
VB6Variant::Long(3) |
a \ b |
BinaryExpression |
eval_binary → arith(IntDiv) (integer path) |
VB6Variant::Long(3) |
a / b |
BinaryExpression |
eval_binary → arith(Divide) (float path) |
VB6Variant::Double(3.333...) |
a ^ 2 |
BinaryExpression |
eval_binary → arith(Exponent) (float path) |
VB6Variant::Double(100.0) |
a Mod b |
BinaryExpression |
eval_binary → arith(Modulus) (integer path) |
VB6Variant::Long(1) |
"Done:" & result |
BinaryExpression |
eval_binary → string coercion + concat |
VB6Variant::String("Done:1") |