Skip to content

Commit 61a8b18

Browse files
authored
Merge pull request #18 from ECP-Solutions/vba_advanced_scripting_module_system
Vba advanced scripting module system
2 parents ecd50eb + ff7f7d4 commit 61a8b18

21 files changed

+1769
-567
lines changed

CHANGELOG.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,261 @@
22

33
All notable changes for ASF. This file combines the release notes from the project's releases.
44

5+
## [v3.0.0] - 2026-02-03
6+
https://github.com/ECP-Solutions/ASF/releases/tag/v3.0.0
7+
8+
## Summary
9+
ASF v3.0.0 is a major-version release that introduces a full ECMAScript-style module system (`import`/`export`) and adds working-directory builtins (`cwd`, `scwd`) together with a single-call `Execute` entry point on the `ASF` UI. Modules are cached on first load, circular dependencies are detected at load time, and relative paths are resolved against the current working directory. Many of the new features are still in beta, awaiting community support and feedback, or under continuous development.
10+
11+
---
12+
13+
## Breaking Changes
14+
15+
- **File extension adopted: `.vas`.**
16+
`.vas` stands for **V**BA **A**dvanced **S**cripting.
17+
`ReadModuleSource` resolves `.vas` automatically; bare module names without an extension are tried first as-is, then with `.vas` appended.
18+
Existing `.asf` source files must be renamed to `.vas` before use with the module loader.
19+
20+
---
21+
22+
## Highlights
23+
24+
- **Added**
25+
- `.vas` file extension — the official source-file extension for all VBA Advanced Scripting files:
26+
27+
- Module system — `import` / `export` statements with ECMAScript semantics:
28+
```javascript
29+
// ── Named exports (math.vas) ──────────────────────
30+
fun add(a, b) {
31+
return a + b;
32+
};
33+
34+
fun multiply(a, b) {
35+
return a * b;
36+
};
37+
38+
PI = 3.14159;
39+
40+
export { add, multiply, PI };
41+
42+
// ── Named imports (main_math.vas) ─────────────────
43+
scwd(wd);
44+
import { add, multiply, PI } from './math.vas';
45+
46+
result = add(5, 3);
47+
area = PI * multiply(5, 5);
48+
return `5 + 3 = ${result}, Circle area: ${area}`;
49+
// => "5 + 3 = 8, Circle area: 78.53975"
50+
```
51+
52+
```javascript
53+
// ── Default export (calculator.vas) ───────────────
54+
fun Calculator() {
55+
return {
56+
add: fun(a, b) { return a + b; },
57+
subtract: fun(a, b) { return a - b; }
58+
};
59+
};
60+
61+
export default Calculator;
62+
63+
// ── Default import (main_calculator.vas) ──────────
64+
scwd(wd);
65+
import calc from './calculator.vas';
66+
67+
calculator = calc();
68+
return(calculator.add(10, 5));
69+
// => "15"
70+
```
71+
72+
```javascript
73+
// ── Namespace import (utils.vas) ──────────────────
74+
fun formatName(first, last) {
75+
return first + ' ' + last;
76+
};
77+
78+
fun uppercase(str) {
79+
return str.toUpperCase();
80+
};
81+
82+
export { formatName, uppercase };
83+
84+
// ── Namespace usage (main_utils.vas) ──────────────
85+
scwd(wd);
86+
import * as utils from './utils.vas';
87+
88+
name = utils.formatName('John', 'Doe');
89+
return utils.uppercase(name);
90+
// => "JOHN DOE"
91+
```
92+
93+
```javascript
94+
// ── Mixed: default + named (lib.vas) ──────────────
95+
fun helper() {
96+
return 'Helper function';
97+
};
98+
99+
fun main() {
100+
return 'Main function';
101+
};
102+
103+
VERSION = '1.0.0';
104+
105+
export default main;
106+
export { helper, VERSION };
107+
108+
// ── Mixed imports (app.vas) ────────────────────────
109+
scwd(wd);
110+
import mainFunc, { helper, VERSION } from './lib.vas';
111+
112+
return `${mainFunc()} | ${helper()} | Version: ${VERSION}`;
113+
// => "Main function | Helper function | Version: 1.0.0"
114+
```
115+
116+
- Aliasing with `as` in both import and export specifier lists:
117+
```javascript
118+
import { add as sum } from './math.vas';
119+
export { localName as publicName };
120+
```
121+
122+
- Working-directory builtins (`cwd` / `scwd`):
123+
```javascript
124+
// scwd(path) — set current working directory
125+
// cwd() — return current working directory
126+
scwd(wd); // set to injected path
127+
currentPath = cwd(); // read it back
128+
// currentPath === wd
129+
```
130+
131+
- `Execute(filePath)` method on the `ASF` façade — single call to read, compile, and run a `.vas` file and return its result:
132+
```vba
133+
Dim eng As New ASF
134+
eng.InjectVariable "wd", ThisWorkbook.path
135+
result = eng.Execute(ThisWorkbook.path & "\main_math.vas")
136+
' result === "5 + 3 = 8, Circle area: 78.53975"
137+
```
138+
139+
- **Internal core changes**:
140+
- **Parser** (`ASF_Parser.cls`):
141+
- Four identifiers are now intercepted during tokenization and emitted as `["KEYWORD", …]` tokens instead of `["IDENT", …]`: `import`, `export`, `default`, `as`
142+
- Matching is case-sensitive; only the lower-case forms are recognised as keywords
143+
144+
- **Compiler** (`ASF_Compiler.cls`):
145+
- New instance properties `CurrentModulePath` and `IsModuleMode` set by the VM loader before each module compilation
146+
- New `ParseImportStatement` method — parses all five supported import forms and produces an `Import` AST node
147+
- New `ParseExportStatement` method — parses named-brace, default-expression, and declaration export forms and produces an `Export` AST node
148+
- `CompileProgram` main loop now checks for `KEYWORD / import` and `KEYWORD / export` tokens before falling through to the existing statement collector; matched statements are added directly to `stmtsAST` via `GoTo Compiler_MainLoop`
149+
150+
- **VM** (`ASF_VM.cls`):
151+
- New builtins `cwd` and `scwd` in the Call-case builtin dispatcher; both read/write `GLOBALS_.CURRENT_MODULE_PATH`
152+
- New `ExecModImport` — resolves module path, calls `LoadModule`, then binds default, namespace, or named imports into the caller's scope; named-export values are resolved through `EvalExprNode` (Variable fallback to `gFuncTable`) so that top-level `fun` declarations export correctly
153+
- New `ExecModExport` — writes named or default export values into `gModuleExports` for the currently executing module path
154+
- New `LoadModule` — orchestrates the full module lifecycle: circular-dependency check against `gLoadingModules`, source read, compilation via a fresh `ASF_Compiler` instance, scope creation, statement execution, pending-export post-processing, and default-export extraction; caches the result in `gModuleRegistry`
155+
- New `ResolveModulePath` — for paths beginning with `./` or `../`, prepends the current working directory (normalised to forward-slash); otherwise returns the path unchanged
156+
- New `ReadModuleSource` — delegates to `ResolveModulePath`, appends `.vas` if the file does not exist without it, raises error 9012 if still missing, then delegates to `ReadTextFile`
157+
158+
- **Globals** (`ASF_Globals.cls`):
159+
- `gModuleRegistry` (`ASF_Map`) — caches loaded module objects keyed by resolved path
160+
- `gModuleExports` (`ASF_Map`) — maps each module path to its live exports map during execution
161+
- `gLoadingModules` (`Collection`) — stack of paths currently being loaded; used for circular-dependency detection
162+
- `CURRENT_MODULE_PATH` (`String`) — the working directory used by `ResolveModulePath` and read/written by `cwd`/`scwd`
163+
- All four members are initialised in `ASF_InitGlobals`
164+
165+
- **ASF UI** (`ASF.cls`):
166+
- New `Execute(filePath As String)` method — reads, compiles, and runs a `.vas` file in a single call; returns the program output
167+
- New `WorkingDir` property (get/set) — direct access to `GLOBALS_.CURRENT_MODULE_PATH`
168+
- New `ClearModuleCache` method — resets `gModuleRegistry`, `gModuleExports`, `gLoadingModules`, and `CURRENT_MODULE_PATH`
169+
- New `ReadTextFile(filePath)` method — exposes the VM's binary-stream file reader
170+
171+
- **Technical Details**:
172+
- **Import AST Node**:
173+
```
174+
Import {
175+
type: "Import"
176+
source: String // raw string-literal value from 'from' clause
177+
defaultImport: String // present only for default-import forms
178+
namespaceImport: String // present only for * as X forms
179+
namedImports: Collection // present only when { … } specifiers exist;
180+
} // each item is an ImportSpecifier
181+
```
182+
183+
- **ImportSpecifier AST Node**:
184+
```
185+
ImportSpecifier {
186+
type: "ImportSpecifier"
187+
imported: String // name as it appears in the exporting module
188+
local: String // name bound in the importing scope (differs when 'as' used)
189+
}
190+
```
191+
192+
- **Export AST Node** (three shapes):
193+
```
194+
// default form
195+
Export {
196+
type: "Export"
197+
isDefault: True
198+
expression: ExprNode // parsed expression after 'default'
199+
}
200+
201+
// named-brace form
202+
Export {
203+
type: "Export"
204+
isDefault: False
205+
namedExports: Collection // each item is an ExportSpecifier
206+
}
207+
208+
// declaration form (export fun …)
209+
Export {
210+
type: "Export"
211+
isDefault: False
212+
declarationType: "function"
213+
declarationName: String // function name; main loop re-parses the declaration
214+
}
215+
```
216+
217+
- **ExportSpecifier AST Node**:
218+
```
219+
ExportSpecifier {
220+
type: "ExportSpecifier"
221+
local: String // name in the module's own scope
222+
exported: String // name exposed to importers (differs when 'as' used)
223+
}
224+
```
225+
226+
- **Module Object** (stored in `gModuleRegistry`):
227+
```
228+
Module (ASF_Map) {
229+
path: String // resolved absolute path used as cache key
230+
loaded: Boolean // True after full execution completes
231+
exports: ASF_Map // live named-export bindings (name → value)
232+
defaultExport: Variant // present only when module contains 'export default'
233+
}
234+
```
235+
236+
- **New Keywords** (tokenised as `["KEYWORD", value]`):
237+
```
238+
"import" "export" "default" "as"
239+
```
240+
241+
- **Error Messages**:
242+
- `"Unexpected end after import"` — import statement truncated (Compiler, #8001)
243+
- `"Expected 'as' after * in import"` — namespace import missing `as` (Compiler, #8002)
244+
- `"Expected identifier after 'as'"` — `as` not followed by a name (Compiler, #8003 / #8004 / #8011)
245+
- `"Invalid import syntax"` — unrecognised token after `import` (Compiler, #8005)
246+
- `"Expected 'from' in import statement"` — missing `from` clause (Compiler, #8006)
247+
- `"Expected string literal for module path"` — non-string after `from` (Compiler, #8007)
248+
- `"Unexpected end after export"` — export statement truncated (Compiler, #8010)
249+
- `"Expected function name after 'fun'"` — `export fun` not followed by name (Compiler, #8012)
250+
- `"Invalid export syntax"` — unrecognised token after `export` (Compiler, #8013)
251+
- `"Export 'X' not found in module 'Y'"` — named import references missing key (VM, #9001)
252+
- `"Circular dependency detected: X"` — module re-entered while still loading (VM, #9010)
253+
- `"Failed to load module: X"` — source read raised an error (VM, #9011)
254+
- `"Module file not found: X"` — path does not exist after `.vas` fallback (VM, #9012)
255+
256+
---
257+
258+
**Full Changelog**: https://github.com/ECP-Solutions/ASF/compare/v2.0.8...v3.0.0
259+
5260
## [v2.0.8] - 2026-02-01
6261
https://github.com/ECP-Solutions/ASF/releases/tag/v2.0.8
7262

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/ECP-Solutions/ASF?style=plastic)](https://github.com/ECP-Solutions/ASF/releases/latest)
88
[![Mentioned in Awesome VBA](https://awesome.re/mentioned-badge.svg)](https://github.com/sancarn/awesome-vba)
99

10-
> **Modern JavaScript-like scripting for VBA projects.** No COM dependencies. No migration required. Just drop in and start using `map`/`filter`/`reduce`, classes with inheritance, closures, regex, and more—all inside your existing Excel, Access, or Office VBA code.
10+
> **Modern JavaScript-like scripting for VBA projects.** No COM dependencies. No migration required. Just drop in and start using `map`/`filter`/`reduce`, classes with inheritance, closures, regex, modules import/export—all inside your existing Excel, Access, or Office VBA code.
1111
1212
```vb
1313
' Before: 30+ lines of VBA boilerplate with ScriptControl
@@ -454,6 +454,57 @@ return `${obj2.a}; ${obj2.b}; ${obj2.c}; ${obj2.d}` //=> 1; 2; 3; 4
454454
return `${rest}` //=> [ 3, 4, 5 ]
455455
```
456456

457+
#### 8. Module system
458+
459+
Starting with `ASFv3.0.0`, users can perform secure, shared script invocation without exposing raw VBA host access through safe `import`/`export` keywords. For example, place this code in a module named `math.vas`
460+
461+
```js
462+
// Named exports
463+
fun add(a, b) {
464+
return a + b;
465+
};
466+
467+
fun multiply(a, b) {
468+
return a * b;
469+
};
470+
471+
PI = 3.14159;
472+
473+
export { add, multiply, PI };
474+
```
475+
476+
In a file named `main_math.vas` place this code
477+
478+
```js
479+
scwd(wd);
480+
import { add, multiply, PI } from './math.vas';
481+
result = add(5, 3);
482+
area = PI * multiply(5, 5);
483+
return `5 + 3 = ${result}, Circle area: ${area}`;// => 78.53975
484+
```
485+
486+
Next, in the VBA IDE, place this code in a new Excel workbook and save it in the same folder as `math.vas` and `main_math.vas` files
487+
488+
```vb
489+
Private Sub math_test()
490+
Dim wd As String
491+
Dim eng As New ASF
492+
Dim result As String
493+
494+
wd = ThisWorkbook.path
495+
With eng
496+
.InjectVariable "wd", wd
497+
result = CStr(.Execute(wd & "\main_math.vas"))
498+
End With
499+
End Sub
500+
```
501+
502+
After execution, the code returns
503+
504+
```
505+
5 + 3 = 8, Circle area: 78.53975
506+
```
507+
457508
---
458509

459510
## Performance & Benchmarks

docs/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This document describes the runtime API exposed by ASF scripts and the VM builti
3232
## Runtime model & conventions
3333

3434
- **Program**: a compiled AST that can be executed by the VM. The ASF host exposes `.Compile(script)` → programIndex and `.Run(programIndex)` to run. The `Run` method returns the result of the program being executed.
35+
- **Modules**: files with the `.vas` file extension can be executed using the `Execute` method. As with the `Run` method, the result of the program is returned.
3536
- **Scope / closures**: closures capture the environment by reference (shared-write semantics). That means nested functions can mutate outer-scope variables and see changes across closures.
3637
- **Indexing base**: arrays honor `__option_base` set in runtime globals (commonly 0 or 1). All array helpers and methods handle this consistently.
3738
- **Call signature for array callbacks**:

docs/Language reference.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2900,33 +2900,39 @@ print(arr[0]); // Depends on option base setting
29002900
29012901
The following words are reserved and cannot be used as variable names:
29022902
2903+
- `as`
2904+
- `break`
2905+
- `case`
2906+
- `catch`
29032907
- `class`
29042908
- `constructor`
2909+
- `continue`
2910+
- `default`
2911+
- `else`
2912+
- `elseif`
2913+
- `export`
29052914
- `extends`
2906-
- `super`
2907-
- `static`
2915+
- `false`
29082916
- `field`
2909-
- `new`
2917+
- `for`
2918+
- `from`
29102919
- `fun`
2911-
- `return`
29122920
- `if`
2913-
- `else`
2914-
- `elseif`
2915-
- `for`
2916-
- `while`
2921+
- `import`
2922+
- `let`
2923+
- `new`
2924+
- `null`
2925+
- `print`
2926+
- `return`
2927+
- `static`
2928+
- `super`
29172929
- `switch`
2918-
- `case`
2919-
- `default`
2920-
- `break`
2921-
- `continue`
2930+
- `true`
29222931
- `try`
2923-
- `catch`
29242932
- `typeof`
2925-
- `true`
2926-
- `false`
2927-
- `null`
2928-
- `let`
2929-
- `print`
2933+
- `undefined`
2934+
- `while`
2935+
29302936

29312937
### Limitations
29322938

0 commit comments

Comments
 (0)