elixcee
Run, test, and diagnose Excel VBA macros without Microsoft Excel. elixcee is a Rust-powered headless VBA runtime for Linux, macOS, and Windows, with static analysis, property-based workbook testing, and root-cause diagnostics for common Excel operation failures.
The core engine is written in Rust; Python bindings are provided via pyo3 + maturin.
Name
elixcee = Excel + elixir + C
An elixir that cures your Excel dependency — running at C-level speed via Rust.
Comparison with similar tools
| Feature | elixcee | xlwings | LibreOffice UNO | openpyxl | xlcalculator |
|---|---|---|---|---|---|
| Runs VBA macros | Yes | Yes | Yes (subset) | No | No |
| Requires Excel | No | Yes | No | No | No |
| Requires LibreOffice | No | No | Yes | No | No |
| Evaluates formulas | Yes | Yes | Yes | No | Yes |
| macOS/Linux/Windows | Yes | partial | Yes | Yes | Yes |
| Simple Python API | Yes | Yes | No | Yes | Yes |
| Read .xlsx | Yes | Yes | Yes | Yes | Yes |
| Read .ods | Yes | Yes | Yes | No | No |
| Write .xlsx | Yes | Yes | Yes | Yes | No |
| Write .ods | Yes | Yes | Yes | No | No |
| Execution speed | Rust (native) | COM/IPC (slow) | IPC (slow) | — | Python |
Notes:
- xlwings requires Excel for Mac on macOS (via AppleScript) and Excel on Windows (via COM). Linux support requires a running Excel instance or a cloud bridge.
- LibreOffice UNO has a slow startup (≥ 1 s process launch) and a complex API. It runs VBA via LibreOffice's own interpreter, which may not match Excel's behavior exactly.
- openpyxl reads cached formula values from .xlsx files but does not re-evaluate formulas at runtime.
- xlcalculator re-evaluates Excel formulas in Python but has no VBA support.
- elixcee's VBA interpreter covers the subset of VBA used in typical data-processing macros (loops, conditionals, cell read/write, string/math functions, multi-sheet access). Most Excel UI operations such as charting and formatting are unsupported or no-ops.
MsgBoxis handled specially: depending on the mode, it's printed to stdout, collected in JSON output, or raised as an error.
Installation
Development build (from source):
&&
CLI (Windows / Linux / macOS)
Pre-built binaries are available on the Releases page — no Python required.
| Download | Platform |
|---|---|
| elixcee-x86_64-windows.exe | Windows x64 |
| elixcee-x86_64-linux | Linux x64 |
| elixcee-aarch64-macos | macOS Apple Silicon |
Usage
elixcee <vba_file>... <MacroName> [OPTIONS]
Arguments:
<vba_file>... One or more VBA source files (.vbs / .bas / .txt). With
more than one, use Module.Sub to disambiguate same-named
Subs/Functions across modules.
<MacroName> Name of the Sub to execute (last argument)
Options:
--file <path> Load cell data from spreadsheet (.xlsx / .xlsm / .ods)
--sheet <name> Active sheet name (default: first sheet in --file)
--output <path> Save result cells to spreadsheet (.xlsx / .ods)
--json Emit a single JSON object (result or error) instead of plain text
Examples
Run a VBA file and print results to stdout:
elixcee macro.vbs ProcessData
Load data from an Excel file, run a macro, and save the output:
elixcee macro.vbs ProcessData --file input.xlsx --output result.xlsx
Output format — one line per non-empty cell, tab-separated address and value:
A1 Hello
B1 42
A2 3.14
MsgBox calls are printed to stdout.
Multiple files (multi-module projects)
Pass more than one source file to run a project spanning several modules.
Sub/Function names are shared project-wide — use Module.Sub to pick a
specific one if the bare name exists in more than one module (module names
come from Attribute VB_Name if present, else the filename):
elixcee Helpers.bas Main.bas Main.ProcessData
There's no project manifest yet (see docs/agent-contract.md for exactly what is/isn't supported, including how cross-module name collisions are handled).
JSON output (for scripts / AI agents)
Add --json for a single machine-readable JSON object instead of plain text:
elixcee macro.vbs ProcessData --json
Full contract — error codes, exit codes, messages semantics: docs/agent-contract.md.
Static analysis without running the macro
elixcee check inspects one or more .bas files without executing them: parse errors, whether the entrypoint macro exists, undefined Sub/Function calls anywhere in the body, and interactive MsgBox calls. Every positional argument is a file; the entrypoint (if any) is always --entry, never positional — so elixcee check *.bas checks every module in a project without asserting any particular entrypoint.
elixcee check macro.vbs --entry ProcessData --json
Workbook snapshot
elixcee snapshot reads a .xlsx/.xlsm/.ods file directly — no VBA
execution — and prints every sheet's non-empty cells as Markdown by default,
or JSON with --json:
elixcee snapshot Book1.xlsx --json
stable_id is derived from the file's own sheetId when available (else a
positional fallback) — it is not VBA's CodeName property. See
docs/agent-contract.md for the full rationale.
Property-based workbook testing
elixcee test-workbook reruns a macro against a starting workbook many
times with generated boundary-value inputs (blank, 0, 1, -1, near
overflow, empty/short/long strings), checking every run for panics,
runtime errors, timeouts, and Excel error values — each case starts from a
completely fresh workbook state:
# fixture.toml
= "order calculation"
= "orders.xlsx"
= ["Main.bas"]
= "Main.Process"
= 100
= 42
[[]]
= "Input!B2:B10"
= "boundary_numeric"
[[]]
= "Result!A1:F100"
= "no_excel_errors"
elixcee test-workbook fixture.toml --json
A failing case reports its seed and case index so it can be reproduced
exactly: elixcee test-workbook fixture.toml --seed 42 --case 17. Full
schema, strategies, and assertion rules: docs/agent-contract.md.
Excel operation diagnostics
elixcee diagnose runs a macro once and explains why Excel would reject
it — a missing worksheet, a missing workbook, an out-of-bounds array
index, a Copy/Paste shape mismatch, a write to a protected sheet, or a
Copy/Paste that conflicts with a merged-cell layout — with evidence,
instead of a bare error string:
elixcee diagnose Main.bas --file report.xlsx --json Main.Run
Range("A1:C10").Copy followed by Range("E1:F10").PasteSpecial reports
both the shape mismatch and where each statement is:
Writing to a .Protected sheet reports which sheet and how to fix it:
Pasting A1:C10 into E1:G10 when the destination's first row is merged
(E1:G1) but the source's isn't reports the layout conflict and both
locations:
Full classification rules and JSON schema: docs/agent-contract.md.
Diagnosing across generated inputs
elixcee diagnose-workbook combines the two features above: it reruns a
macro across test-workbook's generated cases and classifies whichever
failures it finds, instead of only reporting a bare error string. It's most
useful for input-dependent failures like array-bounds errors, where only
some drawn values trigger the bug — a single diagnose call already finds
structural issues (shape mismatches, merged-cell conflicts, sheet
protection) in one shot, since those don't depend on the input at all:
elixcee diagnose-workbook fixture.toml --json
Same fixture format and --seed/--case replay as test-workbook, plus
--cases N to override the fixture's own case count for one run. Full
schema: docs/agent-contract.md.
Multi-area ranges
Range("A1:A10,C1:C10") — a disjoint, multi-area range — is now
recognized by .Copy, but pasting it is diagnose-only: diagnose/
diagnose-workbook classify why, instead of silently doing nothing:
Union(), Areas, Dim rng As Range/Set object variables, and matching-shape
multi-area paste are now implemented — see "VBA object model" below. The 4 classified
codes above still apply to every multi-area shape that doesn't match exactly (different
area counts or shapes, or either side single-area): full scope in
docs/agent-contract.md.
Hidden row/column evidence
diagnose/diagnose-workbook now report when a .Copy'd range overlaps
hidden rows/columns (read from real XLSX hidden="1" metadata) — not an
error, just a new observations field, present alongside (or instead of)
root_causes:
This is what SpecialCells(xlCellTypeVisible) (below) builds on — plain Copy/Paste
itself is unaffected (hidden cells still copy/paste exactly as before). XLSX only; ODS
is deferred. Full scope: docs/agent-contract.md.
VBA object model
Dim rng As Range
Set rng = Range("A1:B2")
rng.Value = 5 ' real Set reference semantics — an alias, not a copy
Dim u As Range
Set u = Union(Range("A1"), Range("D1"))
Range("C1").Value = u.Areas.Count ' 2
Dim ws As Worksheet
Set ws = ActiveSheet
ws.Range("A1").Value = 1
Range("F1").Value = 7 Mod 3 ' 1
Range("F2").Value = 2 ^ 3 ' 8
Range("F3").Value = 7 \ 3 ' 2 (integer division)
If Not (a And b) Then MsgBox "ok"
With Range("A1:B2")
.Value = 5
End With
Function DoubleIt(x As Integer) As Integer
DoubleIt = x * 2
End Function
Set-assigned Range/Worksheet/Workbook object variables (real reference
semantics), Union/Areas, SpecialCells(xlCellTypeVisible) (built on the hidden
row/column evidence above), matching-shape multi-area Copy/Paste, Mod/\/^, infix
And/Or/Xor/Not (real bitwise semantics on non-Boolean operands), With Range(...),
typed Function parameters/return types, comma-separated multi-declarator Dim
(Dim a As Integer, b As Range), and single-line If cond Then stmt [Else stmt] are all
supported.
Known gaps: multi-area Paste only executes when both sides are multi-area with
matching Areas.Count and per-area shapes — every other combination stays diagnose-only
(see above).
XLSX.read() — @elixcee/xlsx (npm, in development)
A synchronous, WebAssembly-backed XLSX.read(bytes) — no await init() required — is
implemented in the in-development @elixcee/xlsx npm package (not yet published; see
docs/xlsx-architecture.md for the compatibility initiative
and the sync-bridge design). It returns sheet names, !ref, !merges, !rows/!cols
(hidden rows/columns), and per-cell {t, v, f, w, z} — values, formula text, formatted
display strings, and date-typed cells, resolved via real styles.xml/number-format
parsing. Differential-tested against the real xlsx@0.18.5 package: 19/19 MATCH on its
declared scope. Works in both Node (CJS/ESM) and the browser (a "browser" export
condition routes to the inlined-bytes/initSync WASM artifact) — the browser entry
point assumes bundled consumption, though: its shared code has a CJS require('ssf'),
so it's not literal no-build <script type="module"> usage.
Build from source
# binary: target/release/elixcee (or elixcee.exe on Windows)
Quick Start
# Run a VBA macro and get all resulting cells
=
# cells == {(1,1): 1, (2,1): 4, (3,1): 9, (4,1): 16, (5,1): 25}
# Pre-populate cells from Python, then run a macro
=
# 300
# {"total": 300}
# Load cell data from an existing Excel file, then run a macro
=
= # {(row, col): value, ...}
# Store a worksheet formula on a cell and evaluate it
# sum of rows 1-3 in column A
# Control MsgBox behavior
= # silently ignore MsgBox calls (default)
= # raise RuntimeError on MsgBox
Python API
| Method | Description |
|---|---|
Vm(on_msgbox="skip") |
Create a new VM. on_msgbox="error" raises RuntimeError on MsgBox. |
vm.run(vba_code, macro_name) |
Parse and execute the named Sub. |
vm.set_cell(row, col, value) |
Write a value into a cell (1-based). |
vm.get_cell(row, col) |
Read a cell value. Returns None for empty cells. |
vm.cells() |
All non-empty cells as {(row, col): value}. |
vm.variables() |
All VBA variables as {name: value}. |
vm.set_cell_formula(row, col, formula) |
Store a formula (e.g. "=SUM(A1:A3)") and evaluate it. |
vm.set_cell_formula_batch(formulas) |
Set multiple formulas at once: {(row, col): formula_str}. |
vm.recalculate() |
Re-evaluate all formula cells (useful after manual cell writes). |
vm.set_sheet(name) |
Switch the active sheet (creates it if absent). |
vm.active_sheet() |
Name of the currently active sheet. |
vm.sheet_names() |
List of all sheet names. |
vm.get_sheet(name) |
Cells of a named sheet as {(row, col): value}. |
vm.save_workbook(path) |
Save all sheets to .xlsx or .ods. |
vm.cells_df() |
Return the active sheet as a pandas DataFrame (requires pandas). |
elixcee.run_macro(vba, name) |
One-shot: run a macro and return {(row, col): value}. |
elixcee.load_workbook(path) |
Load an .xlsx or .ods file into a Vm. |
Coverage
See FUNCTIONS.md for the complete function and VBA syntax reference, including Excel version for each function.
Highlights:
- Classic (Excel 2003-): SUM, VLOOKUP, IF, PMT, FV, PV, NPER, RATE, IPMT, PPMT, NPV, IRR, MIRR, XNPV, XIRR, DGET, DSUM, DAVERAGE, DCOUNT, DCOUNTA, DMAX, DMIN, and 100+ core functions
- 2007–2019: IFERROR, COUNTIFS/SUMIFS, XOR, IFS, SWITCH, TEXTJOIN, MAXIFS/MINIFS
- 365/2021: XLOOKUP, XMATCH, FILTER, SORT, UNIQUE, SEQUENCE, LET, LAMBDA, MAP, REDUCE
- 2024/365: TEXTSPLIT, TEXTBEFORE, TEXTAFTER, VSTACK, HSTACK, TAKE, DROP, CHOOSECOLS, CHOOSEROWS
- VBA: For/If/While/With/On Error/Function/
Type...End Type/Named Ranges/Array of UDT
Named Ranges
Register a named range in VBA with Range("A1:B5").Name = "MyData", then use the name anywhere a range address is accepted:
Range("MyData").Value = 0 ' write to all cells in the range
For Each cell In Range("MyData") ' iterate over cells
total = total + cell
Next cell
Named ranges are stored on vm.named_ranges (a dict[str, str] mapping lowercase name → address).
Criteria Syntax (COUNTIF / SUMIF / SUMIFS / etc.)
| Criteria | Example | Meaning |
|---|---|---|
| Number | 10 |
Exact numeric match |
| String | "apple" |
Case-insensitive string match |
| Comparison | ">5", "<=10", "<>" |
Numeric comparison |
| Wildcard | "a*", "?bc" |
* = any chars, ? = one char |
Application Object
| Property / Method | Description | Behavior |
|---|---|---|
Application.Calculation = xlCalculationManual |
Disable auto-recalculation | Active |
Application.Calculation = xlCalculationAutomatic |
Enable auto-recalculation + re-evaluate all formula cells | Active |
Application.ScreenUpdating = False/True |
Suppress screen refresh | No-op (no screen) |
Application.EnableEvents = False/True |
Disable/enable event triggers | No-op (no events) |
Application.DisplayAlerts = False/True |
Suppress dialog boxes | No-op (no dialogs) |
Application.StatusBar = "..." / False |
Set/clear status bar text | No-op (no UI) |
Application.Cursor = xlWait / xlDefault |
Change cursor shape | No-op (no UI) |
Application.CutCopyMode = False |
Cancel clipboard mode | Active (clears the modeled clipboard) |
No-op properties are parsed and accepted without error, but have no effect. This allows VBA macro performance patterns (e.g.,
Application.ScreenUpdating = Falseat the start of a macro) to run unchanged.
Not Yet Supported
See FUNCTIONS.md — Not Yet Supported for the full list.
Key gaps by category:
- Statistical: NORM.S.DIST, T.INV, F.DIST, CHISQ.DIST, and more
- Text: REPT, NUMBERVALUE, PHONETIC
- Out of scope: IMAGE (URL image fetch), GROUPBY (pivot aggregation), TRIMRANGE
Status Legend
| Mark | Meaning |
|---|---|
| Done | Implemented and tested |
| TBD | Not yet scheduled |
Development Phases
| Phase | Content | Status |
|---|---|---|
| Phase 1 | Rust project setup + pyo3 Python bindings | Done |
| Phase 2 | VBA parser MVP (Sub/End Sub, assignment, Cells) | Done |
| Phase 3 | Virtual Excel VM (variables, cell storage, interpreter) | Done |
| Phase 3.5 | Excel formula engine (SUM, IF, VLOOKUP, Application.Calculation, etc.) | Done |
| Phase 4 | Control flow (For loop, If/Else, arithmetic expressions) | Done |
| Phase 5 | Python interface (Vm class, run_macro, load_workbook, MsgBox) | Done |
| Phase 6 | Formula function expansion (100+ Excel functions, 118 tests) | Done |
| Phase 7 | Advanced VBA constructs (ElseIf, Exit, For Each, On Error, Function, arrays, While-Wend) | Done |
| Phase 8 | Range API (ClearContents, Offset, Sheets.Cells, WorksheetFunction, multi-sheet) | Done |
| Phase 9 | Multi-sheet support (Sheets HashMap, With Sheets, Python API, load_workbook all sheets) | Done |
| Phase 10 | Worksheet function expansion (math, trig, stats, array/spill, lambda functions) | Done |
| Phase 11 | User-defined types (Type...End Type), named ranges, RANDARRAY, pandas integration (cells_df), type stubs (.pyi) |
Done |
| Phase D1 | Remove rust_xlsxwriter, hand-written XLSX via zip (dependencies: 5→4) | Done |
| Phase D2 | Remove pest/pest_derive, hand-written recursive descent VBA parser (dependencies: 4→3) | Done |
| Phase D3 | Remove calamine from runtime, hand-written XLSX/ODS reader (dependencies: 3→2) | Done |
| Perf R4 | SUM/AVERAGE/MIN/MAX fast path (skip Vec<Variant>), RangeWrite dirty-flag batching |
Done |
| CLI | Standalone elixcee binary; pyo3 made optional; GitHub Actions release workflow |
Done |
| Milestone A | JSON agent contract (--json), error classification, MsgBox message log |
Done |
| Milestone A.1 | JSON contract hardening (serde_json-verified tests, message-log lifecycle, error code docs) |
Done |
| Milestone A.5 | Source location tracking — line/column in parse and runtime errors | Done |
| Milestone B1 | check subcommand — parse diagnostics, entrypoint check, MsgBox/interactive-call detection |
Done |
| Milestone B1.1 | check: undefined Sub/Function call detection, unsupported-construct (no-op) detection |
Done |
| Milestone B2 | Multi-module projects — multiple .bas files, Module.Sub qualified entrypoints, cross-module collision detection |
Done |
| Milestone B3 | Deterministic black-box tests (tests/blackbox.rs, declarative .toml fixtures) |
Done |
| Milestone B4 | snapshot subcommand — read a workbook's cells without executing VBA |
Done |
| Milestone B5a | test-workbook subcommand — property-based testing with generated boundary-value inputs |
Done |
| Milestone B6a | diagnose subcommand — missing sheet/workbook, array-out-of-bounds root causes |
Done |
| Milestone B6b | diagnose: Copy/Paste shape mismatch + clipboard state |
Done |
| Milestone B6c | diagnose: sheet protection (Protect/Unprotect) |
Done |