VB6Interpret / Documentation / Expression Evaluation

VB6 Expression Evaluation Architecture Explained

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.

Key Principle: Expression evaluation is side-effect free with respect to the program state. It reads variable values from the current scope, evaluates function calls, and returns a new 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:

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
📝 Note: The 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:

Vb6parse CST
Concrete Syntax Tree
Evaluator
eval_expr dispatch
VB6Variant
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
🎯 Key Feature: This matches VB6's behavior exactly. In VB6, reading an undeclared variable produces an 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 division
  • Mod — Modulo
  • ^ — Exponentiation

String

  • & — Concatenation

Equality

  • = — Equality
  • <> — Inequality

Ordered Comparison

  • < — Less than
  • <= — Less than or equal
  • > — Greater than
  • >= — Greater than or equal

Logical / Bitwise

  • And
  • Or
  • Xor
  • Eqv
  • Imp

Special

  • Like — Pattern matching
  • Is — 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...
}
⚠️ Euclidean Division: The integer division operator \ 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))
Key Detail: The ^ (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)
Key Difference: Unlike +, 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:

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)
📝 Pattern Syntax: The 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:

Tier 1: Array Reference
Lookup + Index
Tier 2: User Function
Procedure Lookup
Tier 3: Built-in
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:

  1. Iterates over the raw &[VB6Variant] argument slice
  2. Converts each argument to the declared type via vb6runtime::boundary
  3. Attaches parameter index and name information for error reporting
  4. Invokes the body with the converted parameters
  5. 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
}
Why Memoization? Without memoization, patterns like *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:

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
⚠️ Error Granularity: Only binary operators for arithmetic and built-in calls attach the operator node position for precise error highlighting. Comparison operators intentionally produce unpositioned errors (as they always have historically). This is preserved for backward compatibility.

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_literalliteral_value VB6Variant::Long(10)
3 IntegerLiteral eval_literalliteral_value VB6Variant::Long(3)
a \ b BinaryExpression eval_binaryarith(IntDiv) (integer path) VB6Variant::Long(3)
a / b BinaryExpression eval_binaryarith(Divide) (float path) VB6Variant::Double(3.333...)
a ^ 2 BinaryExpression eval_binaryarith(Exponent) (float path) VB6Variant::Double(100.0)
a Mod b BinaryExpression eval_binaryarith(Modulus) (integer path) VB6Variant::Long(1)
"Done:" & result BinaryExpression eval_binary → string coercion + concat VB6Variant::String("Done:1")

Further Reading