One grammar for both dialects — once you have got the code out of the workbook.
VBA and VB6 share a language core, and this engine parses it: conditional compilation kept in the tree, Attribute VB_* lines, line labels, type-hint suffixes, Rem as a statement. Built against Microsoft’s own VBA language specification, which ships in the repository.
The hard part of a VBA project is usually not the parser. It is that VBA does not live on disk as text. That problem is the first section of this page, and we are straight about which half of it we ship.
[MS-CFB] → [MS-OVBA] → §2.4.1 DECOMPRESSION → TEXT
Every other page in this catalog starts with a language. This one starts with a container, because that is where VBA projects actually stall — and because a vendor who skips this section has not done one.
01
The host file
Modern Office — .xlsm, .xlsb, .docm, .pptm — is an OPC package, which is a ZIP archive. The VBA lives in a single entry, typically xl/vbaProject.bin, and that entry is itself an OLE2 compound file. Legacy Office — .xls, .doc, .ppt — is a compound file directly, per Microsoft’s [MS-CFB] specification. Access is a third case: .accdb and .mdb keep VBA inside a database, which in practice means driving Access itself or a third-party extractor.
02
The VBA storage
Inside the compound file, [MS-OVBA] defines a VBA storage: a dir stream holding the project’s module list, names, types, references and offsets, and one stream per module holding a compiled performance cache followed by the source text, starting at the offset the dir stream records. Read the offset wrong and you get p-code as text.
03
Decompression
The dir stream and the module source are held in a “compressed container” using a run-length scheme defined in [MS-OVBA] §2.4.1. It is not Deflate, not gzip, and not something you have a library for by default. You implement it, or you use a tool that has.
04
And then the boring problems
VBA source is not Unicode by default — it is in the host’s ANSI code page, and getting that wrong corrupts identifiers and string literals. Projects can be password-locked, which hides the source in the editor without encrypting it, so the text is still extractable and your client’s security team will still have an opinion. Office executes p-code, not the stored source, so the two can disagree — verify you extracted the code the users actually see. Form resources are binary and are not code. Document modules only mean anything inside their host. And modifying a project breaks its digital signature.
We ship the parser, not the extractor. The engine consumes a character stream; getting VBA out of an Office container is upstream of it and is not in this product. We are telling you that here, in the first section, rather than in a footnote — because it is the step that decides your project plan, and because it is work we can walk you through rather than work we can sell you a jar for.
VBA and VB6 are two dialects, not one language
One grammar covers both, and there is a checkable proof of it in the grammar itself: PtrSafe is in the Declare production, and PtrSafe exists only in VBA 7. A VB6-only grammar has no reason to carry it. Here is where the two actually diverge.
The shared statement core is very large — procedures, declarations, all the control flow, With, error handling, Type, Enum, Declare, the Option statements, line continuation, both comment forms and conditional compilation. These are the differences that reach a buyer.
VB6
VBA
Status
Visual Basic 6.0, 1998. Extended support ended in 2008.
Alive and supported. VBA 7.1 ships with Office.
What it produces
Compiled .exe, .dll, .ocx
Interpreted p-code inside a host document
Project unit
A .vbp file listing modules, forms, references and compiler settings
A project embedded in the host file; no separate project text
Forms
.frm plus a binary .frx. A form file is not pure code: a version header and a nested control-definition block come first.
UserForms live in the binary container, not as text on disk
64-bit API declarations
No PtrSafe — it does not exist in VB6
PtrSafe and LongPtr, required in 64-bit VBA 7
Object model
COM and ActiveX, the VB6 runtime
The host object model — workbooks, worksheets, ranges, recordsets — plus COM
Entry point
Sub Main or a startup form
Event handlers, auto-open procedures, macros the user invokes
In the grammar we ship
Module and class headers, references and the control-definition block are grammar rules
PtrSafe, attribute lines and conditional compilation are grammar rules
Registered file extension today
Standard modules only — class and form files parse as grammar but are not wired up in the shipped configuration. Ask before you plan around them.
One distinction worth making explicitly, because it costs projects money: VB6 and VBA are not VB.NET. VB.NET is a different language on a different runtime. Moving from VB6 or VBA to VB.NET is a migration, not an upgrade — Microsoft’s own upgrade wizard was famously partial — and the object model is usually the expensive half, not the syntax.
What it parses that a regex will not
The details below are the ones that prove a VBA parser is real rather than a keyword scanner. Each is a rule in the grammar we ship.
Input — the same API, declared twice
Ledger.bas
Attribute VB_Name = "Ledger"Option Explicit#Const AUDIT = 1#If VBA7 ThenPrivate Declare PtrSafe Function GetTickCount _
Lib"kernel32" () As LongPtr#ElsePrivate Declare Function GetTickCount _
Lib"kernel32" () As Long#End IfPublic Function Post() As Boolean100On Error GoTo FailDim i%, total&, sql$, started
Post = TrueExit Function
Fail:
Rem swallow and carry onResume NextEnd Function
Both branches of the conditional compilation are real code, and which one is effective depends on the host’s bitness. A tool that pre-resolves #If silently discards half of what you are trying to migrate.
Output — parse tree, by grammar rule
root rule: module
module
├── attributeStmtVB_NAME = "Ledger"
├── moduleOptionOption Explicit
├── macroStmtMACRO_CONST — AUDIT
├── macroStmtMACRO_IF — kept, not evaluated
│ ├── declareStmtPtrSafe, Lib "kernel32"
│ └── declareStmtthe 32-bit form
└── moduleBody
└── functionStmtPost, returns Boolean
├── lineLabel100 — a numeric label
├── onErrorStmtGoTo Fail
├── variableStmt4 declarations, 3 typeHint
├── lineLabelFail:
├── commentREMCOMMENT — in the tree
└── resumeStmtResume Next
Real rule names from the grammar we ship. Two things to notice: the #If is a node with both branches under it rather than a preprocessor decision, and the Rem comment is a statement in the tree — unlike an apostrophe comment, which goes to the hidden channel.
Type hints collide with everything
$ % & @ # ! after an identifier are type declarations, not operators — and # also delimits date literals and introduces conditional compilation. The lexer has to keep date, time and datetime literals distinct from a Double suffix and from #If.
Dim i%, total&, sql$, price@, d#Debug.Print #1/1/2026#; sql
Almost nothing is reserved
The grammar carries an explicit ambiguous-identifier rule listing dozens of keywords — including Attribute, Object, Begin, Version, Lib, Alias and Declare — as legal identifiers, because in twenty-year-old code they are used as ones.
Dim Declare As StringDim Version As Long, Alias As Object
Three lexical mechanisms, one line
A single-line If, a colon statement separator and an underscore line continuation can all appear in the same statement. The continuation is a token rather than whitespace, it may sit mid-argument-list, and a comment cannot be continued.
If q > 0Then total = total + q: _ .Cells(i, 2).Value = q
Attribute lines the editor hides
Attribute VB_Name, VB_Creatable, VB_PredeclaredId, VB_Exposed, VB_GlobalNameSpace and VB_Customizable never appear in the VBA editor, but they are in every exported module and every extracted module stream. They are also how a class declares itself. A parser that has not met them fails on line one.
Two more belong on the same list. GoSub and Return — a second, older intra-procedure call mechanism coexisting with Call — are modeled as their own statements. And On Error Resume Next is in the tree as what it is: the construct that turns every following statement into a potential branch. Any analysis of a VB6 estate that ignores it is wrong, and being able to find every occurrence is one of the more valuable things this engine does.
One module that exercises nine of them at once
This is not a contrived sample. It is what a twenty-year-old Excel module looks like, and every construct marked in it is a rule in the grammar.
Input — a standard module, exported as text
Ledger.bas
Attribute VB_Name = "Ledger"Option ExplicitOption Compare Text#If VBA7 ThenPrivate Declare PtrSafe Function GetTickCount Lib"kernel32" () As LongPtr#ElsePrivate Declare Function GetTickCount Lib"kernel32" () As Long#End IfPrivate Type Entry
Sku As String
Qty As LongEnd TypePublic Function Post(ByVal ws As Object, entries() As Entry) As Boolean100On Error GoTo FailDim i%, total&, sql$, started
started = GetTickCount()
With wsFor i = LBound(entries) To UBound(entries)
If entries(i).Qty > 0Then total = total + entries(i).Qty: _ .Cells(i + 1, 2).Value = entries(i).QtyNext i
.Range("D1").Formula = "=SUM(B:B)"End With sql = "INSERT INTO ledger (sku, qty) " & _"VALUES ('" & entries(0).Sku & "', " & total & ")"GoSub LogIt
Post = TrueExit Function
LogIt:
#If AUDIT Then Debug.Print #1/1/2026#; sql#End IfReturn
Fail:
Rem swallow and carry on, as the original author intendedResume NextEnd Function
Note the last two marked lines together: ws As Object means .Cells is late-bound, so nothing static can prove it is an Excel range — and the SQL is assembled by concatenation across a continuation, so recovering the whole statement is a data-flow problem rather than a parsing one. Once recovered, it goes to our SQL engine. That is the family argument in one line of a real macro.
WHAT THIS ENGINE IS, AND IS NOT
It reads VB6 and VBA. It does not write them.
There is no code generator in this module, so “automated refactoring of your VBA” is not something we are going to promise. In a migration, VB6 and VBA are the source; the target comes from another engine.
That is not a hedge, it is the shape of the work. A VBA modernization almost never ends in VBA. The target is usually C# or Python, and the estate usually contains SQL assembled inside string literals. All three of those are things we parse or generate.
Every Strumenta engine is built on Starlasu, so a VB6 tree, a SQL tree and a Python tree have the same shape and the same traversal model. One transformation walks the source and builds the target, in one model, rather than three tools passing text between them.
Every engine supports LionWeb, so the model interchanges with LionWeb-compliant tooling instead of living inside one process — and every engine is reachable through bindings from Java, Kotlin, Python, TypeScript and C#.
Unlike Java, Python or Kotlin, VBA and VB6 have no excellent free parser. The credible open-source options are a handful of ANTLR grammars of varying completeness, and a VBA parser embedded inside an editor add-in rather than offered as a library. So here the family is a bonus rather than the whole case: you are buying a maintained, supported parser for a language almost nobody productizes — and it happens to compose with everything else in the catalog.
Where the migration goes next
one model, one traversal
SQL engine → What the concatenated string literals turn out to be.
Python engine → A common target, and it has a code generator.
A custom parser → If the container work needs building rather than buying.
What you receive
A JVM library and a command-line tool, the specification the grammar was built from, and a license with support in it.
01The engine
A library and a command-line tool, on Java 11 or later
Kotlin on the JVM, built with ANTLR on Kolasu — the JVM implementation of Starlasu. In process you get the tree and the positioned issue list; on the command line you get the AST serialized as JSON or XML. A license file is registered at start-up; licenses are refreshed from a license service, so tell us early if the engine has to run air-gapped.
library · CLI · license enforced
02The model
56 typed nodes, and visible gaps
Modules, procedures and functions, declarations and constants, every control-flow form, With blocks and member calls, error handling, labels, types and parameters, the literal set and the expression hierarchy — each positioned. Where the AST mapper does not yet model a construct it emits an explicit not-implemented node, so gaps are visible in the tree rather than silently dropped.
typed AST · positioned · gaps visible
03The provenance
Built against Microsoft’s own specification
[MS-VBAL], the VBA Language Specification, is a real versioned Open Specifications document with a formal grammar in it — and it ships inside the repository as the reference the grammar was written against. For a Microsoft legacy language that is a stronger provenance story than most, and it is checkable.
[MS-VBAL] · not reverse-engineered
04The license
Standard, Distribution or Service — support included
Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. Support is part of the license. LionWeb export and bindings from Java, Kotlin, Python, TypeScript and C# come with it.
three tiers · LionWeb · five bindings
What the model is good for
Every procedure, branch, label and API declaration as a typed, positioned node.
Counting modules, procedures, decision points or On Error Resume Next sites is a traversal you write once. We do not ship a metrics dashboard, and we do not ship a resolved dependency graph — late binding and Variant put a hard ceiling on how much of one is knowable from source at all.
Why license this rather than use olevba and grep
The free tools in this ecosystem are real and we use them ourselves. The question is what you need, and for a lot of jobs the honest answer is that you do not need us.
What is actually available for VB6 and VBA, and where each option stops.
Option
What it gives you
Where it stops
olevba and the oletools family
Extraction, and text
They solve the container problem — the hard first half of this page — and hand you source text. They do not give you a syntax tree, so anything structural is still grep.
Rubberduck
A real VBA parser
It is embedded in an editor add-in rather than offered as a library you can drive from a pipeline, and it lives inside the VBA editor rather than in your build.
Public ANTLR grammars
A starting point
Of varying completeness, with no AST, no positions you can rely on, no error-tolerance guarantees and nobody to call. Ours is in that lineage and is several years of work past it.
This engine
A typed, positioned AST
At version 0.9.x, with 26 automated tests, no code generator and the limits printed below. It is the youngest engine in this catalog and we would rather you knew that before the invoice than after.
So: if you need to list macros, or search them textually, olevba and grep will do and we will say so. You buy this when you need structure — control flow, procedure inventories, every Win32 declaration, every error-handling hole — across thousands of modules, and when the result has to feed a migration rather than a spreadsheet.
What to discuss before you license
Nine things, which is the longest list in this catalog. That is proportionate: this is a pre-1.0 engine for a language whose real difficulty is partly outside the parser.
We parse source text; we do not open Office files. Extraction from .xlsm, .xls, .docm or Access containers is a project step, described in the first section. We can walk you through it; it is not in the jar.
Version 0.9.x, with 26 automated tests. Pre-1.0, and the thinnest test suite of the engines we license. There is no configured corpus of real open-source VB6 either — unlike our Java and Kotlin engines. We are not going to write “extensively tested” on this page.
No code generator. This engine reads. Refactoring VBA in place, or rewriting it automatically, is not something it does; a migration uses it as the source side.
Standard modules are what is registered today. Class and form files are covered by grammar rules — module and class headers, references, the control-definition block — but the shipped configuration registers standard modules only. Ask before you plan around class or form files.
Project files are not parsed. There is a project-level node in the AST so you can assemble a whole project into one model, but nothing reads a .vbp to populate it. Binary form resources are not code and are not parsed either.
The AST does not cover everything the grammar parses. Where the mapper has no node yet it emits an explicit not-implemented marker. That is deliberate — the gap is in the tree where you can find it — but it is a gap.
Late binding, Variant and default members bound any static analysis.Dim x with no type is a Variant; CreateObject is late-bound; and whether x = obj is an object assignment or a default-property read depends on the type of obj, which is not decidable from syntax. This is the single biggest source of silent error in VB6 migration and no parser fixes it.
Encoding is your decision. VBA source is conventionally in the host’s ANSI code page, not UTF-8. The engine takes a character stream, so choosing the charset is the caller’s call — and getting it wrong corrupts identifiers and literals in ways that look like parse bugs.
The license is a file, refreshed from a service. The engine registers a license file at start-up and the license expires on the order of a month. In a connected environment that is invisible; in an air-gapped one it needs designing for, so raise it in the first call rather than the last.
What we will ask you
VBA, VB6, or both — and if VBA, which Office applications and versions?
Where is the code today: exported text, or still inside host documents?
Are any projects password-locked, and who can authorize unlocking them?
How many documents, how many modules, how many lines?
Are there VB6 forms and project files, or standard modules only?
32-bit or 64-bit VBA — do you have PtrSafe code?
How much late binding is there? That bounds what a static model can tell you.
What is the target: .NET, C#, Python, a web application, or documentation and inventory?
Is there embedded SQL, and against which database?
What language is your tool written in?
Straight answers
Can you read the macros inside our Excel files?
Not directly, and this is the answer that matters most. VBA does not live on disk as text: in modern Office it sits inside a ZIP entry that is itself an OLE2 compound file, in a compressed container defined by Microsoft’s Office VBA file format specification. We ship the parser, which takes source text. Extracting that text is a step before it — a real, well-understood one that we can walk you through and have done before, but it is not in the product.
Does one grammar really handle both VBA and VB6?
Yes, and there is a checkable proof in the grammar: PtrSafe is in the Declare production, and PtrSafe exists only in VBA 7. The statement-level language is very largely shared, and the differences that matter sit at the edges — 64-bit API declarations on the VBA side, project and form files on the VB6 side, and the host object model each one talks to, which is a semantic concern rather than a syntactic one.
Does it resolve #If conditional compilation?
No, deliberately. Conditional compilation is recognized and kept in the tree with both branches under it, rather than pre-resolved. That is the right behavior for modernization: which branch is effective depends on the host’s bitness and on constants that come from the project settings, and a tool that picks one silently discards half the code you are trying to migrate.
Can it refactor or rewrite our VBA automatically?
No. There is no code generator in this module — it reads VB6 and VBA and does not write them. In a migration this engine is the source side and the target comes from another engine in the family: we ship C# support and a Python code generator, so a VBA-to-C# or VBA-to-Python pipeline stays inside one model.
How mature is it?
Version 0.9.x, with 26 automated tests: the youngest engine in this catalog and the only one still below 1.0. Error tolerance is directly tested, and the grammar was written against Microsoft’s own VBA language specification rather than reverse-engineered. But there is no open-source regression corpus configured, there is no code generator, and the AST does not yet cover everything the grammar parses. If you need something with the track record of our COBOL or RPG engines, this is not that yet, and it is better that you hear it here.
Send us one workbookthe one nobody will open
Start with the extraction, not the parser.
Tell us where the code is — exported modules, or still inside the documents — how many host files there are, and what the target is. We will come back with what getting the source out takes, what the engine reads once it is out, and whether a license is the right purchase or whether this is a services conversation. Sometimes it is the second one.