> JavaScript-like scripting language implemented entirely in VBA. Zero COM dependencies. Native Office object model integration with runtime monkey patching capabilities.
That's bananas!
The specific feature being referred-to by the current title ("Monkey Patching in VBA") is functionality to allow you to override or add additional methods to the stock Office COM objects in this ASF scripting language. That's wild.
The extensibility of the Office object model using VBA is powerful and horrifying.
| Version 1.0.1 | Complete Language Reference |
ASF (Advanced Scripting Framework) is a JavaScript-like scripting language implemented in VBA (Visual Basic for Applications). It provides modern programming features within Excel, Access, and other Office applications.
ASF.clsASF_Compiler.clsASF_Globals.clsASF_Map.clsASF_Parser.clsASF_ScopeStack.clsASF_VM.clsASF_RegexEngine.clsUDFunctions.clsVBAcallBack.clsVBAexpressions.clsVBAexpressionsScope.clsSub HelloWorld()
Dim engine As New ASF
Dim code As String
code = "print('Hello, World!');"
Dim idx As Long
idx = engine.Compile(code)
engine.Run idx
End Sub
Sub RunASFCode()
' Create engine instance
Dim engine As New ASF
' Write ASF code
Dim code As String
code = "let x = 10; print(x * 2);"
' Compile and run
Dim result As Variant
result = engine.Run(engine.compile(code))
End Sub
// Single-line comment
/* Multi-line
comment */
# Python-style comment (also supported)
Statements are terminated by semicolons (;):
let x = 10;
print(x);
Semicolons are optional at the end of the script but required at end of statements blocks:
if (x > 5) {
print('Greater');
}; // Semicolon mandatory here
let y = 20; // Semicolon optional here
ASF is case-sensitive:
let myVar = 10;
let MyVar = 20; // Different variable
Whitespace is generally ignored:
let x=10; // Valid
let y = 20; // More readable
All numbers are floating-point:
let integer = 42;
let decimal = 3.14159;
let negative = -100;
let scientific = 1.5e10;
Strings are enclosed in single quotes:
let name = 'John Doe';
let message = 'Hello, World!';
let empty = '';
Template literals and regex patterns use backticks:
let name = 'Alice';
let greeting = `Hello, ${name}!`; // "Hello, Alice!"
let arr = 'test1test2'.match(`/t(e)(st(\d?))/g`)
let isTrue = true;
let isFalse = false;
Represents intentional absence of value:
let nothing = null;
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, 'two', true, null];
let nested = [[1, 2], [3, 4]];
let empty = [];
let person = {
name: 'John',
age: 30,
email: 'john@example.com'
};
let nested = {
user: {
name: 'Alice',
address: {
city: 'Boston'
}
}
};
typeof 42; // 'number'
typeof 'hello'; // 'string'
typeof true; // 'boolean'
typeof null; // 'null'
typeof []; // 'array'
typeof {}; // 'object'
typeof fun() {}; // 'function'
// Built-in functions
isArray([1, 2, 3]); // true
isNumeric(42); // true
isNumeric('42'); // true
isNumeric('hello'); // false
Variables do not need to be declared; in any case, the interpreter supports the use of the let keyword to assign variables:
x = 10;
let y = 20; // Also valid (converted to simple assignment)
Functions shares scope variables, outer variables can be mutated:
let x = 10;
fun test() {
let x = 20; /* Same variable*/
print(x) /* Outputs: 20 */
};
test();
print(x); // Outputs: 20
let x = 10;
x = 20; /* Reassignment also accepts let x = 20 (does not behave like JavaScript)*/
let arr = [1, 2, 3];
arr[0] = 10; /* Array element assignment */
let obj = { name: 'John' };
obj.name = 'Jane'; /* Property assignment */
let x = 10;
x += 5; // x = x + 5 (15)
x -= 3; // x = x - 3 (12)
x *= 2; // x = x * 2 (24)
x /= 4; // x = x / 4 (6)
x %= 4; // x = x % 4 (2)
x ^= 3; // x = x ^ 3 (8)
x &= 7; // x = x & 7 (string concat)
x |= 2; // x = x | 2 (bitwise OR)
let a = 10, b = 3;
a + b; // 13 (addition)
a - b; // 7 (subtraction)
a * b; // 30 (multiplication)
a / b; // 3.333... (division)
a % b; // 1 (modulus/remainder)
a ^ b; // 1000 (exponentiation)
let x = 10, y = 20;
x == y; // false (equal)
x != y; // true (not equal)
x < y; // true (less than)
x > y; // false (greater than)
x <= y; // true (less than or equal)
x >= y; // false (greater than or equal)
let a = true, b = false;
a && b; // false (AND)
a || b; // true (OR)
!a; // false (NOT)
'Hello' + ' ' + 'World'; // 'Hello World'
'Value: ' + 42; // 'Value: 42'
'Count' & ': ' & 10; // 'Count: 10' (alternative)
let x = 5; // Binary: 101
let y = 3; // Binary: 011
x << 1; // 10 (left shift)
x >> 1; // 2 (right shift)
Compound bitwise assignment:
x <<= 2; // x = x << 2
x >>= 1; // x = x >> 1
let age = 18;
let status = (age >= 18) ? 'adult' : 'minor';
print(status); // 'adult'
// Nested ternary
let score = 85;
let grade = (score >= 90) ? 'A' :
(score >= 80) ? 'B' :
(score >= 70) ? 'C' : 'F';
//rest argument
fun greetAll(greeting, ...names) {
result = greeting + ': ';
for (name of names) {
result = result + name + ', ';
};
return result.slice(0, -2);
};
msg = greetAll('Hello', 'Alice', 'Bob', 'Charlie');
return msg //=> Hello: Alice, Bob, Charlie
//spread operator on arrays
begin = [1]; middle = [2, 3, 4]; end = [5]; combined = [...begin, ...middle, ...end];
print(combined); //=> [ 1, 2, 3, 4, 5 ]
//spread operator on objects
obj1 = {a: 1, b: 2}; obj2 = {c: 3, ...obj1, d: 4};
return `${obj2.a}; ${obj2.b}; ${obj2.c}; ${obj2.d}` //=> 1; 2; 3; 4
From highest to lowest:
( )!, -, typeof, ...^*, /, %+, -<<, >><, >, <=, >===, !=&&||? :=, +=, -=, etc.let x = 10;
if (x > 5) {
print('Greater than 5');
};
if (x > 15) {
print('Greater than 15');
} else {
print('Not greater than 15');
};
// Multiple conditions
if (x > 20) {
print('Greater than 20');
} elseif (x > 10) {
print('Greater than 10');
} elseif (x > 5) {
print('Greater than 5');
} else {
print('5 or less');
};
let day = 3;
switch (day) {
case 1 {
print('Monday');
}
case 2 {
print('Tuesday');
}
case 3 {
print('Wednesday');
}
default {
print('Other day');
};
};
Note: ASF switch statements don’t fall through - no break needed.
// Standard C-style for loop
for (let i = 0, i < 5, i += 1) {
print(i);
};
// Array indices
let arr = [10, 20, 30];
for (let i in arr) {
print(i); /* 1, 2, 3 (indices, 1-based) */
};
// Object keys
let obj = { name: 'John', age: 30 };
for (let key in obj) {
print(key); /* 'name', 'age' */
};
// String indices
let str = 'ABC';
for (let i in str) {
print(i); // 1, 2, 3
};
// Array values
let arr = [10, 20, 30]
for (let val of arr) {
print(val); /* 10, 20, 30 */
};
// String characters
let str = 'ABC';
for (let char of str) {
print(char); /* 'A', 'B', 'C' */
};
// Object values
let obj = { name: 'John', age: 30 };
for (let val of obj) {
print(val); /* 'John', 30 */
};
let i = 0;
while (i < 5) {
print(i);
i = i + 1;
}
// Infinite loop with break
let count = 0;
while (true) {
if (count >= 10) {
break;
};
count = count + 1;
}; print(count); //-->10
// Break: exit loop
for (let i = 0, i < 10, i += 1) {
if (i == 5) {
break; // Exit loop when i is 5
};
print(i);
};
// Continue: skip to next iteration
for (let i = 0, i < 10, i += 1) {
if (i % 2 == 0) {
continue; // Skip even numbers
};
print(i); // Prints odd numbers only
};
fun greet(name) {
print('Hello, ' + name + '!');
};
greet('Alice'); // Hello, Alice!
fun add(a, b) {
return a + b;
};
let result = add(5, 3); // 8
let square = fun(x) {
return x * x;
};
print(square(5)); // 25
let numbers = [1, 2, 3, 4, 5];
// Anonymous function in map
let squared = numbers.map(fun(x) {
return x * x;
}); //--> [ 1, 4, 9, 16, 25 ]
Functions can capture variables from their enclosing scope:
fun makeCounter() {
let count = 0;
return fun() {
count = count + 1;
return count;
};
};
let counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
Functions that accept or return other functions:
fun operate(a, b, operation) {
return operation(a, b)
};
let add = fun(x, y) { return x + y };
let multiply = fun(x, y) { return x * y };
print(operate(5, 3, add)); // 8
print(operate(5, 3, multiply)); // 15
fun factorial(n) {
if (n <= 1) {
return 1;
};
return n * factorial(n - 1);
};
print(factorial(5)); // 120
// Fibonacci
fun fib(n) {
if (n <= 1) {
return n;
};
return fib(n - 1) + fib(n - 2);
};
print(fib(10)); // 55
fun greet(name) {
if (typeof name == 'undefined') {
name = 'Guest';
};
print('Hello, ' + name + '!');
};
greet(); // Hello, Guest!
greet('Alice'); // Hello, Alice!
// Using array as parameter
fun sum(numbers) {
let total = 0;
for (let i = 1, i <= numbers.length, i += 1) {
total += numbers[i];
};
return total;
};
print(sum([1, 2, 3, 4])); // 10
let empty = [];
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, 'two', true, null, [5, 6]];
Important: ASF uses 1-based indexing by default (EXPERIMENTAL: configurable with option base).
let arr = [10, 20, 30, 40];
// 1-based indexing (default)
print(arr[1]); // 10 (first element)
print(arr[4]); // 40 (last element)
// Assignment
arr[2] = 25;
print(arr[2]); // 25
let arr = [1, 2, 3, 4, 5];
print(arr.length); // 5
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
print(matrix[1][1]); // 1
print(matrix[2][3]); // 6
// Check if array
isArray([1, 2, 3]); // true
isArray('hello'); // false
// Flatten nested arrays
let nested = [1, [2, 3], [4, [5, 6]]];
let flat = flatten(nested); // [1, 2, 3, 4, 5, 6]
// Flatten with depth limit
let partial = flatten(nested, 1); // [1, 2, 3, 4, [5, 6]]
// Clone array (deep copy)
let original = [1, 2, [3, 4]];
let copy = clone(original);
Transform each element:
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map(fun(x) {
return x * 2;
});
print(doubled); // [2, 4, 6, 8, 10]
// With index
let indexed = numbers.map(fun(val, idx, arr) {
return val + idx;
});
Select elements that match a condition:
let numbers = [1, 2, 3, 4, 5, 6];
let evens = numbers.filter(fun(x) {
return x % 2 == 0;
});
print(evens); // [2, 4, 6]
Reduce array to single value:
let numbers = [1, 2, 3, 4, 5];
let sum = numbers.reduce(fun(acc, val) {
return acc + val;
}, 1);
print(sum); // 16
// Without initial value (uses first element)
let product = numbers.reduce(fun(acc, val) {
return acc * val;
});
print(product); // 120
Find first matching element:
let users = [
{ name: 'John', age: 25 },
{ name: 'Jane', age: 30 },
{ name: 'Bob', age: 35 }
];
let user = users.find(fun(u) {
return u.age > 28;
});
print(user.name); // 'Jane'
Find index of first matching element:
let numbers = [10, 20, 30, 40, 50];
let idx = numbers.findIndex(fun(x) {
return x > 25;
});
print(idx); // 3 (30 is at index 3, 1-based)
Find from end of array:
let numbers = [10, 20, 30, 20, 10];
let last = numbers.findLast(fun(x) {
return x == 20;
});
print(last); // 20 (last occurrence)
let arr = [1, 2, 3, 2, 1];
print(arr.indexOf(2)); // 2 (first occurrence)
print(arr.lastIndexOf(2)); // 4 (last occurrence)
print(arr.indexOf(5)); // -1 (not found)
let fruits = ['apple', 'banana', 'orange'];
print(fruits.includes('banana')); // true
print(fruits.includes('grape')); // false
Add elements to end:
let arr = [1, 2, 3];
arr.push(4);
arr.push(5, 6);
print(arr); // [1, 2, 3, 4, 5, 6]
Remove last element:
let arr = [1, 2, 3, 4];
let last = arr.pop();
print(last); // 4
print(arr); // [1, 2, 3]
Remove first element:
let arr = [1, 2, 3, 4];
let first = arr.shift();
print(first); // 1
print(arr); // [2, 3, 4]
Add elements to beginning:
let arr = [3, 4];
arr.unshift(1, 2);
print(arr); // [1, 2, 3, 4]
Remove/insert elements:
let arr = [1, 2, 3, 4, 5];
// Remove 2 elements starting at index 2
let removed = arr.splice(2, 2);
print(removed); // [2, 3]
print(arr); // [1, 4, 5]
// Insert elements
arr = [1, 2, 5];
arr.splice(3, 0, 3, 4); // At index 3, remove 0, insert 3, 4
print(arr); // [1, 2, 3, 4, 5]
// Replace elements
arr = [1, 2, 3, 4, 5];
arr.splice(2, 2, 99); // Remove 2 elements, insert 99
print(arr); // [1, 99, 5]
Reverse array in place:
let arr = [1, 2, 3, 4, 5];
arr.reverse();
print(arr); // [5, 4, 3, 2, 1]
Sort array in place:
let numbers = [3, 1, 4, 1, 5, 9];
numbers.sort();
print(numbers); // [1, 1, 3, 4, 5, 9]
// Custom comparator
let words = ['banana', 'apple', 'cherry'];
words.sort(fun(a, b) {
if (a < b) { return -1;};
if (a > b) { return 1;};
return 0;
});
print(words); // ['apple', 'banana', 'cherry']
Extract portion of array:
let arr = [1, 2, 3, 4, 5];
let sub = arr.slice(2, 4); // From index 2 to 4 (exclusive)
print(sub); // [2, 3]
// Negative indices (from end)
let last2 = arr.slice(-2);
print(last2); // [4, 5]
Combine arrays:
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2, [5, 6]);
print(combined); // [1, 2, 3, 4, 5, 6]
Non-mutating versions (return new array):
let arr = [3, 1, 4, 1, 5];
let sorted = arr.toSorted();
print(sorted); // [1, 1, 3, 4, 5]
print(arr); // [3, 1, 4, 1, 5] (unchanged)
let reversed = arr.toReversed();
print(reversed); // [5, 1, 4, 1, 3]
print(arr); // [3, 1, 4, 1, 5] (unchanged)
Create copy with one element changed:
let arr = [1, 2, 3, 4];
let modified = arr.with(2, 99); // Change index 2 to 99
print(modified); // [1, 99, 3, 4]
print(arr); // [1, 2, 3, 4] (unchanged)
Execute function for each element:
let numbers = [1, 2, 3, 4, 5];
numbers.forEach(fun(val, idx) {
print('Index ' + idx + ': ' + val);
});
Test if all elements match condition:
let numbers = [2, 4, 6, 8];
let allEven = numbers.every(fun(x) {
return x % 2 == 0;
});
print(allEven); // true
Test if any element matches condition:
let numbers = [1, 3, 5, 8];
let hasEven = numbers.some(fun(x) {
return x % 2 == 0;
});
print(hasEven); // true
Remove duplicates:
let arr = [1, 2, 2, 3, 3, 3, 4];
let unique = arr.unique();
print(unique); // [1, 2, 3, 4]
Access with negative indices:
let arr = [1, 2, 3, 4, 5];
print(arr.at(1)); // 1 (first element)
print(arr.at(-1)); // 5 (last element)
print(arr.at(-2)); // 4 (second to last)
Get [index, value] pairs:
let arr = ['a', 'b', 'c'];
let pairs = arr.entries();
// [[1, 'a'], [2, 'b'], [3, 'c']]
Convert to string:
let arr = [1, 2, 3];
print(arr.join()); // '1, 2, 3'
print(arr.join('-')); // '1-2-3'
print(arr.toString()); // '1, 2, 3'
Create array from iterable:
let str = 'hello';
let chars = [].from(str);
print(chars); // ['h', 'e', 'l', 'l', 'o']
// With mapping function
let doubled = [].from([1, 2, 3], fun(x) {
return x * 2;
});
print(doubled); // [2, 4, 6]
Create array from arguments:
let arr = [].of(1, 2, 3, 4);
print(arr); // [1, 2, 3, 4]
Copy elements within array:
let arr = [1, 2, 3, 4, 5];
arr.copyWithin(1, 3); // Copy from index 3 to index 1
print(arr); // [1, 3, 4, 4, 5]
Remove element at index:
let arr = [1, 2, 3, 4, 5];
arr.delete(3); // Remove element at index 3
print(arr); // [1, 2, 4, 5]
let person = {
name: 'John Doe',
age: 30,
email: 'john@example.com'
};
// Dot notation
print(person.name); // 'John Doe'
// Bracket notation
print(person['age']); // 30
// Dynamic property access
let prop = 'email';
print(person[prop]); // 'john@example.com'
person.age = 31;
person['city'] = 'Boston';
person.country = 'USA'; // Add new property
let company = {
name: 'Tech Corp',
address: {
street: '123 Main St',
city: 'Boston',
zip: '02101'
},
employees: [
{ name: 'Alice', role: 'Developer' },
{ name: 'Bob', role: 'Designer' }
]
};
print(company.address.city); // 'Boston'
print(company.employees[1].name); // 'Alice' (1-based)
let calculator = {
add: fun(a, b) {
return a + b;
},
multiply: fun(a, b) {
return a * b;
}
};
print(calculator.add(5, 3)); // 8
print(calculator.multiply(4, 7)); // 28
let key = 'status';
let obj = {};
obj[key] = 'active';
print(obj.status); // 'active'
Get array of all property names:
let person = { name: 'John', age: 30, city: 'Boston' };
let props = person.keys();
print(props); // ['name', 'age', 'city']
// Iterate over keys
person.keys().forEach(fun(key) {
print(key + ': ' + person[key]);
});
Get array of all property values:
let scores = { math: 85, english: 92, science: 78 };
let vals = scores.values();
print(vals); // [85, 92, 78]
// Calculate total
let total = scores.values().reduce(fun(sum, val) {
return sum + val;
}, 0);
print(total); // 255
Get array of [key, value] pairs:
let config = { host: 'localhost', port: 8080, ssl: true };
let pairs = config.entries();
print(pairs); // [['host', 'localhost'], ['port', 8080], ['ssl', true]]
// Convert to different format
config.entries().forEach(fun(pair) {
print(pair[1] + '=' + pair[2]);
});
// Output:
// host=localhost
// port=8080
// ssl=true
Get number of properties:
let obj = { a: 1, b: 2, c: 3 };
print(obj.size()); // 3
print(obj.length()); // 3 (alias)
let empty = {};
print(empty.size()); // 0
Check if property exists:
let user = { name: 'Alice', email: 'alice@example.com' };
print(user.hasKey('name')); // true
print(user.has('email')); // true (alias)
print(user.hasKey('phone')); // false
// Safe property access
if (user.has('address')) {
print(user.address.city);
} else {
print('No address available');
};
Check if object has no properties:
let obj1 = {};
print(obj1.isEmpty()); // true
let obj2 = { x: 1 };
print(obj2.isEmpty()); // false
// Clear and check
obj2.clear();
print(obj2.isEmpty()); // true
Get property value with optional default:
let config = { timeout: 30, retry: 3 };
// Get existing property
print(config.get('timeout')); // 30
// Get with default for missing property
print(config.get('maxSize', 100)); // 100 (returns default)
// Without default returns empty
print(config.get('missing')); // '' (empty)
// Use in conditionals
let port = config.get('port', 8080);
print('Port: ' + port); // Port: 8080
Set property value:
let user = { name: 'John' };
// Add new property
user.set('age', 30);
print(user.age); // 30
// Update existing property
user.set('name', 'Jane');
print(user.name); // Jane
// Chain multiple sets
user.set('city', 'Boston').set('country', 'USA');
// Set with dynamic key
let key = 'status';
user.set(key, 'active');
print(user.status); // active
Remove property:
let person = { name: 'Alice', age: 25, temp: 'delete-me' };
// Delete property
person.delete('temp');
print(person.hasKey('temp')); // false
// Using alias
person.remove('age');
print(person); // { name: 'Alice' }
// Delete non-existent property (safe)
person.delete('notThere'); // No error
Remove all properties:
let data = { a: 1, b: 2, c: 3, d: 4 };
print(data.size()); // 4
data.clear();
print(data.size()); // 0
print(data.isEmpty()); // true
// Object is now empty but still usable
data.set('x', 10);
print(data.x); // 10
Create deep copy of object:
let original = {
name: 'John',
scores: [85, 90, 78],
address: {
city: 'Boston',
zip: '02101'
}
};
let copy = original.clone();
// Modify copy
copy.name = 'Jane';
copy.scores[1] = 95;
copy.address.city = 'New York';
// Original unchanged
print(original.name); // John
print(original.scores[1]); // 85 (1-based indexing)
print(original.address.city); // Boston
// Copy modified
print(copy.name); // Jane
print(copy.scores[1]); // 95
print(copy.address.city); // New York
Merge another object’s properties:
let defaults = {
timeout: 30,
retry: 3,
verbose: false
};
let userConfig = {
timeout: 60,
cache: true
};
// Merge userConfig into defaults (mutates defaults)
defaults.merge(userConfig);
print(defaults);
// {
// timeout: 60, // Overwritten
// retry: 3, // Preserved
// verbose: false, // Preserved
// cache: true // Added
// }
// Nested merge (overwrites nested objects)
let obj1 = { a: 1, nested: { x: 10, y: 20 } };
let obj2 = { b: 2, nested: { y: 30, z: 40 } };
obj1.merge(obj2);
print(obj1);
// {
// a: 1,
// b: 2,
// nested: { y: 30, z: 40 } // obj2.nested replaces obj1.nested
// }
// Safe merge pattern (preserve original)
let merged = original.clone().merge(updates);
Execute function for each property:
let scores = { math: 85, english: 92, science: 78 };
// With value only
scores.forEach(fun(val) {
print(val);
});
// Output: 85, 92, 78
// With value and key
scores.forEach(fun(val, key) {
print(key + ': ' + val);
});
// Output:
// math: 85
// english: 92
// science: 78
// Modify during iteration (affects original)
let data = { a: 1, b: 2, c: 3 };
data.forEach(fun(val, key) {
data[key] = val * 2;
});
print(data); // { a: 2, b: 4, c: 6 }
// Count values matching condition
let count = 0;
scores.forEach(fun(val) {
if (val > 80) {
count = count + 1;
}
});
print(count); // 2
Transform all values, return new object:
let prices = { apple: 1.50, banana: 0.75, orange: 2.00 };
// Double all prices
let doubled = prices.map(fun(val) {
return val * 2;
});
print(doubled); // { apple: 3, banana: 1.5, orange: 4 }
// With key parameter
let formatted = prices.map(fun(val, key) {
return key + ': $' + val;
});
print(formatted);
// { apple: 'apple: $1.5', banana: 'banana: $0.75', orange: 'orange: $2' }
// Transform nested objects
let users = {
user1: { name: 'John', age: 30 },
user2: { name: 'Jane', age: 25 }
};
let names = users.map(fun(user) {
return user.name;
});
print(names); // { user1: 'John', user2: 'Jane' }
// Original unchanged (non-mutating)
print(prices.apple); // 1.5
Filter properties by condition, return new object:
let scores = { math: 85, english: 92, science: 78, history: 95 };
// Filter passing grades (>= 90)
let passing = scores.filter(fun(val) {
return val >= 90;
});
print(passing); // { english: 92, history: 95 }
// Filter by key
let user = {
name: 'John',
_id: 12345,
email: 'john@example.com',
_internal: true
};
let publicFields = user.filter(fun(val, key) {
return !key.startsWith('_');
});
print(publicFields); // { name: 'John', email: 'john@example.com' }
// Complex filtering
let products = {
item1: { name: 'Widget', price: 10, inStock: true },
item2: { name: 'Gadget', price: 25, inStock: false },
item3: { name: 'Tool', price: 15, inStock: true }
};
let available = products.filter(fun(item) {
return item.inStock && item.price < 20;
});
print(available); // { item1: {...}, item3: {...} }
Test if any property matches condition:
let scores = { math: 85, english: 92, science: 78 };
// Any score above 90?
let hasExcellent = scores.some(fun(val) {
return val > 90;
});
print(hasExcellent); // true
// Any failing grade?
let hasFailing = scores.some(fun(val) {
return val < 60;
});
print(hasFailing); // false
// With key
let config = {
debugMode: false,
logging: true,
verbose: false
};
let anyEnabled = config.some(fun(val, key) {
return val == true && key.startsWith('log');
});
print(anyEnabled); // true
// Validation example
let user = { name: '', email: 'valid@example.com' };
let hasEmptyField = user.some(fun(val) {
return val == '';
});
print(hasEmptyField); // true
Test if all properties match condition:
let scores = { math: 85, english: 92, science: 88 };
// All passing (>= 60)?
let allPassing = scores.every(fun(val) {
return val >= 60;
});
print(allPassing); // true
// All excellent (>= 90)?
let allExcellent = scores.every(fun(val) {
return val >= 90;
});
print(allExcellent); // false
// Validation example
let requiredFields = { name: 'John', email: 'john@example.com', age: 30 };
let allPresent = requiredFields.every(fun(val) {
return val != null && val != '';
});
print(allPresent); // true
// Type checking
let numbers = { a: 1, b: 2, c: 3 };
let allNumeric = numbers.every(fun(val) {
return typeof val == 'number';
});
print(allNumeric); // true
Combine object methods for complex transformations:
let inventory = {
apple: { price: 1.50, quantity: 10 },
banana: { price: 0.75, quantity: 5 },
orange: { price: 2.00, quantity: 0 },
grape: { price: 3.50, quantity: 8 }
};
// Filter in-stock items, then get total value
let totalValue = inventory
.filter(fun(item) {
return item.quantity > 0;
})
.map(fun(item) {
return item.price * item.quantity;
})
.values()
.reduce(fun(sum, val) {
return sum + val;
}, 0);
print(totalValue); // 43.75
// Transform and validate
let users = {
user1: { name: 'John', age: 30, active: true },
user2: { name: 'Jane', age: 25, active: false },
user3: { name: 'Bob', age: 35, active: true }
};
let activeUserNames = users
.filter(fun(u) { return u.active; })
.map(fun(u) { return u.name; })
.values();
print(activeUserNames); // ['John', 'Bob']
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
print('Hello, I am ' + this.name);
}
getAge() {
return this.age;
}
}
let person = new Person('Alice', 25);
person.greet(); // Hello, I am Alice
print(person.getAge()); // 25
print(person.name); // Alice
Declare instance fields with the field keyword:
class Rectangle {
field width = 0, height = 0;
constructor(w, h) {
this.width = w;
this.height = h;
}
getArea() {
return this.width * this.height;
}
}
let rect = new Rectangle(10, 5);
print(rect.getArea()); // 50
// Single field
field x;
field x = 10;
// Multiple fields (comma-separated)
field x, y, z;
field x = 1, y = 2, z = 3;
// Mixed with/without initializers
field x = 1, y, z = 3;
// Multiple declarations
field width = 0, height = 0;
field color = 'black';
field name;
Parent class fields (if inheritance)
Current class fields
Constructor body
class Point { field x = 0, y = 0; constructor(x, y) { // Fields already initialized to 0 this.x = x; // Now set to parameter value this.y = y; } }
Methods that belong to the class, not instances:
class MathHelper {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
static PI = 3.14159; // Note: static fields via method
}
// Call without creating instance
print(MathHelper.add(5, 3)); // 8
print(MathHelper.multiply(4, 7)); // 28
Classes can extend other classes:
class Animal {
field name, species;
constructor(name, species) {
this.name = name;
this.species = species;
}
speak() {
print(this.name + ' makes a sound');
}
}
class Dog extends Animal {
field breed;
constructor(name, breed) {
super(name, 'Dog'); // Call parent constructor
this.breed = breed;
}
speak() {
print(this.name + ' barks!');
}
getBreed() {
return this.breed;
}
}
let dog = new Dog('Rex', 'Labrador');
dog.speak(); // Rex barks!
print(dog.getBreed()); // Labrador
print(dog.species); // Dog
Call parent constructor:
class Shape {
field x, y;
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Circle extends Shape {
field radius;
constructor(x, y, r) {
super(x, y); // Must call parent constructor
this.radius = r;
}
getArea() {
return 3.14159 * this.radius * this.radius;
}
}
The this keyword refers to the current instance:
class Counter {
field count = 0;
increment() {
this.count = this.count + 1;
}
getValue() {
return this.count;
}
}
let c = new Counter();
c.increment();
c.increment();
print(c.getValue()); // 2
class BankAccount {
field balance = 0;
field accountNumber;
field owner;
constructor(owner, accountNum) {
this.owner = owner;
this.accountNumber = accountNum;
}
deposit(amount) {
if (amount > 0) {
this.balance = this.balance + amount;
return true;
}
return false;
}
withdraw(amount) {
if (amount > 0 && amount <= this.balance) {
this.balance = this.balance - amount;
return true;
}
return false;
}
getBalance() {
return this.balance;
}
getInfo() {
return this.owner + ' (' + this.accountNumber + '): $' + this.balance;
}
static create(owner) {
let accNum = 'ACC' + Math.floor(Math.random() * 10000);
return new BankAccount(owner, accNum);
}
}
let account = new BankAccount('John Doe', 'ACC12345');
account.deposit(1000);
account.withdraw(250);
print(account.getInfo()); // John Doe (ACC12345): $750
ASF v3.0.0 introduces a full ECMAScript-style module system using import and export statements to organize code across multiple files.
ASF source files use the .vas extension (VBA Advanced Scripting):
project/
├── main.vas
├── math.vas
├── utils.vas
└── lib.vas
Module paths automatically append .vas if omitted. Example: './math' resolves to './math.vas'.
Import specific exports by name:
import { add, multiply, PI } from './math.vas';
result = add(5, 3);
area = PI * multiply(5, 5);
Import the default export:
import Calculator from './calculator.vas';
calc = Calculator();
sum = calc.add(10, 5);
Import all exports as a namespace object:
import * as utils from './utils.vas';
name = utils.formatName('John', 'Doe');
upperName = utils.uppercase(name);
Import both default and named exports:
import mainFunc, { helper, VERSION } from './lib.vas';
print(mainFunc()); // Uses default export
print(helper()); // Uses named export
print(VERSION); // Uses named export
Rename imports to avoid conflicts:
import { add as sum, multiply as times } from './math.vas';
result = sum(2, 3);
product = times(4, 5);
Export multiple values by name:
// math.vas
fun add(a, b) {
return a + b;
};
fun multiply(a, b) {
return a * b;
};
PI = 3.14159;
export { add, multiply, PI };
Export a single default value:
// calculator.vas
fun Calculator() {
return {
add: fun(a, b) { return a + b; },
subtract: fun(a, b) { return a - b; }
};
};
export default Calculator;
Export a function declaration:
export fun processData(data) {
return data.map(fun(x) { return x * 2; });
};
Rename exports:
fun internalName() {
return 'Internal implementation';
};
export { internalName as publicName };
./, ../) resolve against cwd()Use cwd() and scwd() to manage module resolution:
// Set working directory before imports
scwd(wd);
// Relative imports now resolve from wd
import { add } from './math.vas';
import { helper } from '../lib/utils.vas';
// Get current directory
currentDir = cwd();
From VBA, use the Execute method to run .vas files:
Sub RunModule()
Dim eng As New ASF
' Set working directory
eng.InjectVariable "wd", ThisWorkbook.Path
' Execute module file
Dim result As Variant
result = eng.Execute(ThisWorkbook.Path & "\main.vas")
Debug.Print result
End Sub
Or manually compile and run:
Sub RunModuleManual()
Dim eng As New ASF
Dim code As String
' Set working directory
eng.WorkingDir = ThisWorkbook.Path
' Read and execute
code = eng.ReadTextFile(ThisWorkbook.Path & "\main.vas")
Dim idx As Long
idx = eng.Compile(code)
eng.Run idx
Debug.Print eng.OUTPUT_
End Sub
Clear the module cache to force re-execution:
' From VBA
eng.ClearModuleCache
' Modules will execute fresh on next import
project/
├── main.vas ' Entry point
├── math.vas ' Math utilities
├── utils.vas ' String utilities
├── lib.vas ' Shared library
└── calculator.vas ' Calculator class
main.vas:
scwd(wd);
import { add, multiply } from './math.vas';
import * as utils from './utils.vas';
import Calculator from './calculator.vas';
calc = Calculator();
result = calc.add(add(5, 3), multiply(2, 4));
name = utils.formatName('ASF', 'Framework');
return `${name}: ${result}`;
math.vas:
fun add(a, b) { return a + b; };
fun multiply(a, b) { return a * b; };
PI = 3.14159;
export { add, multiply, PI };
utils.vas:
fun formatName(first, last) {
return first + ' ' + last;
};
fun uppercase(str) {
return str.toUpperCase();
};
export { formatName, uppercase };
calculator.vas:
fun Calculator() {
return {
add: fun(a, b) { return a + b; },
subtract: fun(a, b) { return a - b; },
multiply: fun(a, b) { return a * b; },
divide: fun(a, b) { return a / b; }
};
};
export default Calculator;
Module-related errors:
try {
import { missing } from './nonexistent.vas';
} catch (e) {
print('Module load failed: ' + e);
};
Common errors:
Output to debug/console:
print('Hello, World!');
print(42);
print([1, 2, 3]);
print({ name: 'John', age: 30 });
Return type as string:
// Basic types
typeof 42; // 'number'
typeof 'hello'; // 'string'
typeof true; // 'boolean'
typeof null; // 'null'
typeof undefined; // 'undefined'
typeof [1, 2, 3]; // 'array'
typeof {}; // 'object'
typeof fun() {}; // 'function'
// VBA object types (when AppAccess = True)
typeof $1; // 'object: <Workbook>'
typeof $1.sheets; // 'object: <Sheets>'
typeof $1.sheets(1); // 'object: <Worksheet>'
typeof $1.sheets(1).range('A1'); // 'object: <Range>'
// VBA Collections and Dictionaries
// typeof <Collection instance> // 'object: <Collection>'
// typeof <Dictionary instance> // 'object: <Dictionary>'
Check if value is array:
isArray([1, 2, 3]); // true
isArray('hello'); // false
isArray(null); // false
Check if value is numeric:
isNumeric(42); // true
isNumeric('42'); // true
isNumeric('hello'); // false
isNumeric(true); // false
Create array of numbers:
range(5); // [0, 1, 2, 3, 4]
range(1, 6); // [1, 2, 3, 4, 5]
range(0, 10, 2); // [0, 2, 4, 6, 8]
range(10, 0, -2); // [10, 8, 6, 4, 2]
Flatten nested arrays:
let nested = [1, [2, 3], [4, [5, 6]]];
flatten(nested); // [1, 2, 3, 4, 5, 6]
flatten(nested, 1); // [1, 2, 3, 4, [5, 6]]
Deep copy arrays/objects:
let original = [1, 2, [3, 4]];
let copy = clone(original);
copy[3][1] = 99;
print(original[3][1]); // 4 (unchanged)
Iterate over array:
let nums = [1, 2, 3, 4, 5];
foreach(nums, fun(val, idx, arr) {
print('Index ' + idx + ': ' + val)
});
Iterate over object:
let scores = {math: 85, english: 92, science: 78};
foreach(scores, fun(val, key) {
s = s + val; c += 1
}); return('Average: ' + s/c); // Average: 85
Create regex engine:
let re = regex(`hello`, true); // Case-insensitive
let re2 = regex(`\\d+`); // Matches digits
Get current working directory:
let currentPath = cwd();
print(currentPath); // Shows current working directory
Set current working directory for module resolution:
scwd(wd); // Set working directory
// Relative imports now resolve from this directory
import { add } from './math.vas';
Usage pattern with VBA:
Dim eng As New ASF
eng.InjectVariable "wd", ThisWorkbook.Path
result = eng.Execute(ThisWorkbook.Path & "\main.vas")
Inside the .vas file:
scwd(wd); // Use injected working directory
import { helper } from './utils.vas';
let str = 'Hello';
print(str.charAt(0)); // 'H' (0-based like JavaScript)
print(str.charCodeAt(0)); // 72 (ASCII code of 'H')
Access with negative indices:
let str = 'Hello';
print(str.at(0)); // 'H' (first char)
print(str.at(-1)); // 'o' (last char)
print(str.at(-2)); // 'l' (second to last)
let str = 'Hello';
print(str.length); // 5
let str = 'Hello World';
print(str.toLowerCase()); // 'hello world'
print(str.toUpperCase()); // 'HELLO WORLD'
let str = ' hello ';
print(str.trim()); // 'hello'
print(str.trimStart()); // 'hello '
print(str.trimEnd()); // ' hello'
let str = 'abc';
print(str.repeat(3)); // 'abcabcabc'
let str = '5';
print(str.padStart(3, '0')); // '005'
print(str.padEnd(3, '0')); // '500'
let str = 'hello world hello';
print(str.indexOf('hello')); // 0 (first occurrence)
print(str.lastIndexOf('hello')); // 12 (last occurrence)
print(str.indexOf('xyz')); // -1 (not found)
let str = 'hello world';
print(str.includes('world')); // true
print(str.includes('xyz')); // false
let str = 'hello world';
print(str.startsWith('hello')); // true
print(str.endsWith('world')); // true
print(str.startsWith('world')); // false
let str = 'hello world';
print(str.slice(0, 5)); // 'hello'
print(str.slice(6)); // 'world'
print(str.slice(-5)); // 'world' (last 5 chars)
let str = 'hello world';
print(str.substring(0, 5)); // 'hello'
print(str.substring(6, 11)); // 'world'
let str1 = 'Hello';
let str2 = 'World';
print(str1.concat(' ', str2)); // 'Hello World'
let str = 'apple,banana,orange';
let fruits = str.split(',');
print(fruits); // ['apple', 'banana', 'orange']
let chars = 'hello'.split('');
print(chars); // ['h', 'e', 'l', 'l', 'o']
// With limit
let limited = 'a,b,c,d'.split(',', 2);
print(limited); // ['a', 'b']
let str = 'hello world hello';
// Replace first occurrence
print(str.replace('hello', 'hi')); // 'hi world hello'
// Replace all occurrences
print(str.replaceAll('hello', 'hi')); // 'hi world hi'
// With function
let result = str.replace('hello', fun(match) {
return match.toUpperCase();
});
print(result); // 'HELLO world hello'
// With regex
let str2 = 'hello123world456';
let result2 = str2.replace(`/\\d+/`, 'X'); // Replace digits
print(result2); // 'helloXworld456'
let str = 'The price is $10 and $20';
// Match first occurrence
let match = str.match('$10');
print(match); // ['$10']
// Match with regex (all digits)
let numbers = str.match(`/\\d+/g`);
print(numbers); // ['10', '20']
// matchAll returns array of all matches
let all = str.matchAll(`/\\d+/g`);
// [['10'], ['20']]
let str = 'hello';
print(str.toString()); // 'hello'
print(str.valueOf()); // 'hello'
print(''.fromCharCode(72)); // 'H'
let a = 'apple';
let b = 'banana';
print(a.localeCompare(b)); // -1 (a < b)
print(b.localeCompare(a)); // 1 (b > a)
print(a.localeCompare(a)); // 0 (equal)
Use backticks (`) for template literals:
let name = 'Alice';
let greeting = `Hello, ${name}!`;
print(greeting); // Hello, Alice!
Any expression can be embedded:
let a = 5, b = 10;
print(`${a} + ${b} = ${a + b}`); // 5 + 10 = 15
let items = [1, 2, 3];
print(`Array length: ${items.length}`); // Array length: 3
let message = `Line 1
Line 2
Line 3`;
// Note: Actual newline handling depends on VBA
let user = { name: 'John', role: 'admin' };
let status = `User ${user.name} (${user.role == 'admin' ? 'Administrator' : 'User'})`;
print(status); // User John (Administrator)
let users = [
{ name: 'Alice', score: 85 },
{ name: 'Bob', score: 92 }
];
for (let i = 1, i <= users.length, i += 1) {
let user = users[i];
print(`${user.name}: ${user.score >= 90 ? 'A' : 'B'}`);
}
// Output:
// Alice: B
// Bob: A
let re = regex(`pattern`);
re.init(`\\d+`); // Just pattern
re.init(`hello`, true); // Pattern + case-insensitive
re.init(`hello`, true, false, true); // Pattern + flags
i - Case-insensitiveg - Global (find all matches)m - Multilines - Dot matches newline (dotAll)Test if pattern matches:
let re = regex(`\\d+`);
print(re.test('hello123')); // true
print(re.test('hello')); // false
Execute regex and get first match:
let re = regex(`(\\d+)-(\\d+)`);
let result = re.exec('Phone: 555-1234');
print(result); // ['555-1234', '555', '1234']
// result[1] = full match, result[2+] = capture groups
Get all matches (global flag required):
let re = regex(`\\d+`);
let matches = re.execAll('Numbers: 10, 20, 30');
print(matches); // [['10'], ['20'], ['30']]
Replace pattern matches:
let re = regex(`\\d+`);
let str = 'Price: $10 and $20';
let result = re.replace(str, 'XX');
print(result); // Price: $XX and $XX
Split string by pattern:
let re = regex(`[,;]`);
let str = 'apple,banana;orange';
let parts = re.split(str);
print(parts); // ['apple', 'banana', 'orange']
Escape special regex characters, and the first character of the given string:
let escaped = regex().escape('a.b*c?');
print(escaped); // '\a\.b\*c\?'
let re = regex(`hello`);
// Get/Set pattern
print(re.getPattern()); // 'hello'
re.setPattern(`world`);
print(re.getPattern()); // 'world'
// Get/Set flags
print(re.getIgnoreCase()); // true
re.setIgnoreCase(false);
print(re.getMultiline());
re.setMultiline(true);
print(re.getDotAll());
re.setDotAll(true);
let str = 'The numbers are 10 and 20';
let nums = str.match('/\\d+/g');
print(nums); // ['10', '20']
let str = 'hello123world456';
// Replace with string
let r1 = str.replace('/\\d+/', 'X');
print(r1); // 'helloXworld456' (first only)
// Replace all with global flag
let r2 = str.replace('/\\d+/g', 'X');
print(r2); // 'helloXworldX'
// Replace with function
let r3 = str.replace('/\\d+/g', fun(match) {
return '[' + match + ']'
});
print(r3); // 'hello[123]world[456]'
// Email validation (simple)
let email = regex(`^[^@]+@[^@]+\\.[^@]+$`);
print(email.test('user@example.com')); // true
// Phone number
let phone = regex(`^\\d{3}-\\d{3}-\\d{4}$`);
print(phone.test('555-123-4567')); // true
// URL
let url = regex(`^https?:\\/\\/`);
print(url.test('https://example.com')); // true
// Hexadecimal color
let color = regex(`^#[0-9a-fA-F]{6}$`);
print(color.test('#FF5733')); // true
// Extract all words
let words = regex(`\\w+`);
let text = 'Hello, World! 123';
print(words.execAll(text)); // [['Hello'], ['World'], ['123']]
try {
let x = 10 / 0; // May cause error in some contexts
print('Result: ' + x);
} catch {
print('An error occurred!');
}
try {
try {
// Inner operation
let result = riskyOperation();
} catch {
print('Inner error');
};
// Outer operation
anotherOperation();
} catch {
print('Outer error');
};
fun safeDivide(a, b) {
try {
if (b == 0) {
return null;
}
return a / b;
} catch {
return null;
};
}
let result = safeDivide(10, 2);
if (result == null) {
print('Division failed');
} else {
print('Result: ' + result);
};
fun validateUser(user) {
try {
if (typeof(user.name) != 'string') {
return false;
}
if (typeof(user.age) != 'number') {
return false;
}
if (user.age < 0) {
return false;
}
return true;
} catch {
return false;
};
};
let user = { name: 'John', age: 30 };
if (validateUser(user)) {
print('User is valid');
} else {
print('Invalid user data');
};
Use @(...) syntax to evaluate with VBA-Expressions:
// Call VBA-Expressions functions
let a = @({1;0;4});
let b = @({1;1;6});
let c = @({-3;0;-10});
print(@(MROUND(LUDECOMP(ARRAY(a;b;c));4)))
From VBA, inject values into ASF:
Dim engine As New ASF
engine.InjectVariable "userData", Array("John", 30, "john@example.com")
Dim code As String
code = "print(userData[1]);" ' Prints: John
Dim idx As Long
idx = engine.Compile(code)
engine.Run idx
Dim engine As New ASF
Dim code As String
code = "let x = 10; let y = 20; return(x + y);"
Dim idx As Long
idx = engine.Compile(code)
engine.Run idx
' Get output
Dim result As Variant
result = engine.OUTPUT_
Debug.Print result ' Prints: 30
ASF v3.1.0+ provides native Office object support, enabling seamless interaction with Excel, Word, PowerPoint, Outlook, and Access directly from ASF scripts. This integration includes full property chaining, method invocation, and bidirectional array marshaling.
Control Office object access with the AppAccess security property:
Dim engine As New ASF
' Enable Office object access (default: False)
engine.AppAccess = True
' Now scripts can access Office objects via $1, $2, etc.
pid = engine.Compile("return $1.name")
result = engine.Run(pid, ThisWorkbook)
Security Note: AppAccess is False by default. Enable only when scripts need to interact with Office applications.
Dim engine As New ASF
engine.AppAccess = True
' Access workbook properties
pid = engine.Compile("return $1.name")
wbName = engine.Run(pid, ThisWorkbook)
' Access sheets
pid = engine.Compile("return $1.sheets.count")
sheetCount = engine.Run(pid, ThisWorkbook)
' Access specific sheet
pid = engine.Compile("return $1.sheets(1).name")
sheetName = engine.Run(pid, ThisWorkbook)
// Read from range
let data = $1.sheets(1).range('A1:C10').value2;
// Write to range
$1.sheets(1).range('D1').value2 = 'Total';
// Property chaining
let cellValue = $1.sheets(1).range('A1').value2;
Dim engine As New ASF
engine.AppAccess = True
' Get paragraph text
pid = engine.Compile("return $1.paragraphs(1).range.text")
text = engine.Run(pid, ActiveDocument)
' Count paragraphs
pid = engine.Compile("return $1.paragraphs.count")
count = engine.Run(pid, ActiveDocument)
Dim engine As New ASF
engine.AppAccess = True
' Get slide count
pid = engine.Compile("return $1.slides.count")
slideCount = engine.Run(pid, ActivePresentation)
' Access shapes on a slide
pid = engine.Compile("return $1.slides(1).shapes.count")
shapeCount = engine.Run(pid, ActivePresentation)
ASF v3.1.1+ automatically converts between ASF jagged arrays and VBA 2D arrays when interacting with Office objects:
Dim engine As New ASF
engine.AppAccess = True
' ASF jagged array → VBA 2D array (automatic)
Dim code As String
code = "let data = [['id', 'name'], [1, 'John'], [2, 'Jane']]; " & _
"$1.sheets(1).range('A1:B3').value2 = data;" ' Automatic conversion!
engine.Run engine.Compile(code), ThisWorkbook
' VBA 2D array → ASF jagged array (automatic)
code = "let range = $1.sheets(1).range('A1:B3').value2; " & _
"return range[0][0];" ' Automatic conversion!
result = engine.Run(engine.Compile(code), ThisWorkbook)
How It Works:
Range.Value2, automatic conversion to VBA 2D arraysRange.Value2, automatic conversion to ASF jagged arraysThe typeof operator provides detailed type information for VBA objects:
// VBA Collections
let coll = /* New Collection */;
typeof coll; // 'object: <Collection>'
// Scripting Dictionary
let dict = /* CreateObject("Scripting.Dictionary") */;
typeof dict; // 'object: <Dictionary>'
// Excel objects
typeof $1; // 'object: <Workbook>'
typeof $1.sheets; // 'object: <Sheets>'
typeof $1.sheets(1); // 'object: <Worksheet>'
typeof $1.sheets(1).range('A1'); // 'object: <Range>'
// Other Office applications
// Word: 'object: <Document>', 'object: <Paragraphs>'
// PowerPoint: 'object: <Presentation>', 'object: <Slides>'
Disable AppAccess by default: Only enable when needed
Validate input: Sanitize user input before passing to Office objects
Limit scope: Pass specific objects (e.g., single worksheet) rather than entire workbook
Error handling: Wrap Office interactions in try-catch blocks
' Good: Limited scope engine.AppAccess = True Set targetSheet = ThisWorkbook.Sheets("Data") result = engine.Run(pid, targetSheet)
' Better: Disable after use engine.AppAccess = True result = engine.Run(pid, ThisWorkbook) engine.AppAccess = False
ASF v3.1.2+ supports COM object prototype extension (monkey patching), allowing you to add custom methods to Office objects at runtime.
Prototype extension enables you to:
Define prototype methods using the prototype.COM.ObjectType methodName() syntax:
// Syntax: prototype.COM.<ObjectType> <methodName>(<parameters>) { <body> }
prototype.COM.Range formatAsHeader() {
this.Font.Bold = true;
this.Font.Size = 14;
this.Interior.Color = 15592941; // Light blue
return this;
}
// Add a method to format currency
prototype.COM.Range formatCurrency() {
this.NumberFormat = "$#,##0.00";
this.Font.Bold = true;
this.Font.Color = 192; // Dark red
return this;
}
// Usage
$1.Range('B2:B10').formatCurrency();
// Add a method to count words in a paragraph
prototype.COM.Paragraph countWords() {
let text = this.Range.Text;
let words = text.split(' ').filter(fun(word) {
return word.trim().length > 0;
});
return words.length;
}
// Usage
let wordCount = $1.Paragraphs(1).countWords();
// Add a method to apply consistent formatting
prototype.COM.Slide applyTemplate() {
this.Background.Fill.ForeColor.RGB = 16777215; // White
if (this.Shapes.Count > 0) {
this.Shapes(1).TextFrame.TextRange.Font.Name = 'Calibri';
this.Shapes(1).TextFrame.TextRange.Font.Size = 24;
}
return this;
}
// Usage
$1.Slides(1).applyTemplate();
prototype.COM.ListRow asDictionary() {
let headers = this.parent.listcolumns;
let values = this.range.value2;
let result = {};
for (let i = 1, i <= headers.count, i += 1) {
let columnName = headers.item(i).name;
let cellValue = values[1][i];
result.set(columnName, cellValue);
}
return result;
}
// Usage
let customer = $1.ListObjects('Customers').ListRows(1).asDictionary();
let name = customer.get('Name');
let email = customer.get('Email');
prototype.COM.Recordset toJSON() {
let results = [];
this.MoveFirst();
while (!this.EOF) {
let record = {};
for (let i = 0, i < this.Fields.Count, i += 1) {
let fieldName = this.Fields(i).Name;
let fieldValue = this.Fields(i).Value;
record.set(fieldName, fieldValue);
}
results.push(record);
this.MoveNext();
}
return results;
}
// Usage (Access)
let data = $1.OpenRecordset('SELECT * FROM Products').toJSON();
Prototype methods can return this to enable fluent interfaces:
// Define chainable methods
prototype.COM.Range setBold() {
this.Font.Bold = true;
return this;
}
prototype.COM.Range setColor(color) {
this.Font.Color = color;
return this;
}
prototype.COM.Range center() {
this.HorizontalAlignment = -4108; // xlCenter
return this;
}
// Chain method calls
$1.Range('A1:C1')
.setBold()
.setColor(255) // Red
.center()
.formatCurrency();
When OverrideCollMethods = True, Office collections gain JavaScript array methods that work seamlessly with prototype methods:
// Process all rows in a table
let processedData = $1.ListObjects('Sales').ListRows
.map(fun(row) {
return row.asDictionary();
})
.filter(fun(dict) {
return dict.get('Amount') > 1000;
})
.map(fun(dict) {
return {
customer: dict.get('Customer'),
amount: dict.get('Amount'),
formatted: '$' + dict.get('Amount').toString()
};
});
this ContextWithin prototype methods, this refers to the COM object instance:
prototype.COM.Worksheet findLastRow(column) {
// 'this' refers to the Worksheet object
let lastRow = this.Cells(this.Rows.Count, column).End(-4162).Row; // xlUp
return lastRow;
}
prototype.COM.Workbook saveBackup() {
// 'this' refers to the Workbook object
let backupName = this.Path + '\\' + this.Name + '.backup';
this.SaveCopyAs(backupName);
return this;
}
Enable prototype functionality in your VBA code:
Sub UsePrototypeMethods()
Dim engine As New ASF
' Required settings
engine.AppAccess = True ' Enable Office object access
engine.OverrideCollMethods = True ' Enable collection method override
' Define prototype method
Dim prototypeCode As String
prototypeCode = "prototype.COM.Range highlight() {" & _
" this.Interior.Color = 65535;" & _
" this.Font.Bold = true;" & _
" return this;" & _
"};"
' Use prototype method
Dim usageCode As String
usageCode = "$1.Range('A1:A10').highlight();"
' Execute
Dim pid As Long
pid = engine.Compile(prototypeCode + usageCode)
engine.Run pid, ThisWorkbook.Sheets(1)
End Sub
Prototype methods work with any Office COM object:
Application, Workbook, Workbooks, Worksheet, WorksheetsRange, ListObject, ListRow, ListColumn, Chart, PivotTableApplication, Document, Documents, SelectionParagraph, Paragraphs, Table, Tables, RangeApplication, Presentation, PresentationsSlide, Slides, Shape, ShapesApplication, Form, Forms, Report, ReportsRecordset, TableDef, QueryDefApplication, MailItem, ContactItem, FolderAttachment, Recipient, AccountHandle prototype method errors using try-catch:
prototype.COM.Range safeFormat() {
try {
this.NumberFormat = "0.00%";
this.Font.Color = 255;
return true;
} catch (error) {
print('Formatting failed: ' + error);
return false;
}
}
// Usage with error handling
try {
let success = $1.Range('A1').safeFormat();
if (!success) {
print('Range formatting failed');
}
} catch (error) {
print('Prototype method error: ' + error);
}
this for Chainability// Good - enables chaining
prototype.COM.Range formatHeader() {
this.Font.Bold = true;
this.Font.Size = 12;
return this; // Enable chaining
}
// Less useful - breaks chaining
prototype.COM.Range formatHeader() {
this.Font.Bold = true;
this.Font.Size = 12;
return true; // Returns boolean instead
}
prototype.COM.Range setBackgroundColor(colorValue) {
if (typeof colorValue != 'number') {
print('Error: Color must be a number');
return this;
}
this.Interior.Color = colorValue;
return this;
}
// Good - clear and descriptive
prototype.COM.Range formatAsCurrency() { }
prototype.COM.Range highlightNegativeValues() { }
prototype.COM.ListRow convertToDictionary() { }
// Poor - vague or confusing
prototype.COM.Range doStuff() { }
prototype.COM.Range format() { } // Too generic
prototype.COM.ListRow convert() { } // Unclear what it converts to
OverrideCollMethods = True) can impact memory usage// Good
fun calculateTotal(items) {
return items.reduce(fun(sum, item) {
return sum + item.price;
}, 0)
};
// Bad
let total = 0;
for (let i = 1, i <= items.length, i = i + 1) {
total = total + items[i].price;
};
// Separate concerns
fun validateInput(data) {
// Validation logic
}
fun processData(data) {
if (!validateInput(data)) {
return null;
}
// Processing logic
}
fun formatOutput(result) {
// Formatting logic
};
// Variables and functions: camelCase
let userName = 'John';
fun calculateTotal() { }
// Classes: PascalCase
class UserAccount { }
class BankTransaction { }
// Constants: UPPER_CASE (by convention)
let MAX_SIZE = 100;
let DEFAULT_TIMEOUT = 30;
// Boolean variables: is/has prefix
let isValid = true;
let hasPermission = false;
// Good - functional approach
let doubled = numbers.map(fun(x) { return x * 2; });
let evens = numbers.filter(fun(x) { return x % 2 == 0; });
// Slower - manual loops
let doubled = [];
for (let i = 1, i <= numbers.length, i = i + 1) {
doubled.push(numbers[i] * 2);
};
// Good
let len = arr.length;
for (let i = 1, i <= len, i = i + 1) {
// Process arr[i]
}
// Less efficient
for (let i = 1, i <= arr.length, i = i + 1) {
// arr.length evaluated each iteration
};
// Good
fun processItems(items) {
let total = 0;
let count = items.length;
// Process locally
return { total: total, count: count };
}
// Less efficient (global access)
let globalTotal = 0;
fun processItems(items) {
// Access global repeatedly
}
fun divide(a, b) {
if (typeof a != 'number' || typeof b != 'number') {
return null;
};
if (b == 0) {
return null;
};
return a / b;
};
fun parseUserData(jsonString) {
try {
// Risky operation
return JSON.parse(jsonString);
} catch {
return null;
};
};
let largeArray = range(1, 100000);
// Use array
processData(largeArray);
// Clear when done
largeArray = [];
// Good - flat structure
fun processUser(user) {
if (!user) return null;
if (!user.name) return null;
if (!user.email) return null;
return formatUser(user);
}
// Bad - deep nesting
fun processUser(user) {
if (user) {
if (user.name) {
if (user.email) {
return formatUser(user);
};
};
};
return null;
};
Control array indexing (0-based or 1-based):
// Set at program start
option base 0; // Use 0-based indexing
option base 1; // Use 1-based indexing (default)
let arr = [10, 20, 30];
print(arr[0]); // Depends on option base setting
The following words are reserved and cannot be used as variable names:
asbreakcasecatchclassconstructorcontinuedefaultelseelseifexportextendsfalsefieldforfromfunifimportletnewnullprintreturnstaticsuperswitchtruetrytypeofundefinedwhileasync/await supportPromise or callback patterns=>)const declaration (use let)var (use let)Potential future additions:
For bug reports, feature requests, or contributions, please contact the ASF development team.
Copyright 2026 W. García
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
End of Documentation
| Version 1.0.1 | Last Updated:February 2026 |